docs(solutions): i18next empty-placeholder blank-render learning + Translation Placeholder concept

This commit is contained in:
gsxdsm
2026-06-05 14:23:27 -07:00
parent 349ea2a60a
commit 9843fb9fcb
2 changed files with 68 additions and 0 deletions

View File

@@ -13,6 +13,9 @@ User-level settings persisted server-side that apply across all Surfaces and all
### Three-Tier Setting
The named persistence pattern for a user preference on the dashboard: a device-local cache for instant reads, a write-through to Global Settings so other Surfaces see it, and a hydrate-on-mount from the server when no local value exists. A local or in-flight user choice always wins over server hydration, and changes propagate to other open tabs.
### Translation Placeholder
An empty-string value for a catalog key in a non-English locale, marking "not yet translated." Placeholders are intentionally backfilled when keys are added; at runtime they are treated as missing (never rendered), falling back through the locale chain to English. A non-empty value — even an English one left in a non-en catalog — is rendered as-is.
### Supported Locale
A language tag in the closed set Fusion ships translations for. Any external tag (browser, environment, flag) is normalized into this set or rejected — never passed through raw. Chinese tags route by script and region so Traditional-script users are never silently served Simplified, and the two Chinese variants never collapse into a generic base tag.

View File

@@ -0,0 +1,65 @@
---
title: Empty-string locale placeholders render blank UI (i18next returnEmptyString default)
date: 2026-06-05
category: ui-bugs
module: i18n
problem_type: ui_bug
component: frontend
symptoms:
- "Buttons, labels, and dialog copy render completely blank in non-English locales"
- "Inline English defaults passed to t(\"key\", \"Default\") are ignored — blank wins"
- "No console errors; en locale looks perfect; only translated locales affected"
root_cause: wrong_api
resolution_type: config_fix
severity: high
related_components:
- dashboard
- tooling
tags: [i18next, returnEmptyString, locale-placeholders, translation-fallback, i18n-extract, catalog-pruning]
---
# Empty-string locale placeholders render blank UI (i18next returnEmptyString default)
## Problem
The repo's translator workflow backfills `""` placeholders into non-en catalogs for untranslated keys, on the assumption that empty values fall back to English at runtime. They don't: i18next's default `returnEmptyString: true` treats `""` as a *found* value, so es/fr/ko/zh users saw blank buttons, nav labels, and dialog copy for every new key — even though components pass inline English defaults (`t("key", "Default")`).
## Symptoms
- New UI strings render blank in any non-English locale while en looks correct.
- The inline second-argument default to `t()` does not rescue it — `""` short-circuits the fallback chain entirely.
- Verified empirically (i18next 26.x): `t("empty", "InlineDefault")` returns `""` when the active locale defines the key as `""`.
## What Didn't Work
- Assuming the standing convention was safe because hundreds of `""` placeholders pre-existed — the convention had been silently rendering blanks all along for any key reached in a non-en locale.
- Relying on inline `t()` defaults as a safety net — they only apply when the key is *missing*, not empty.
## Solution
One config line in the shared i18next init (`packages/i18n/src/config.ts`, `baseInitOptions()`):
```ts
returnEmptyString: false,
```
With this set, `""` values are treated as missing and fall through the fallback chain (`fallbackLng` → en, or the inline default). Locale files stay untouched, the `""`-placeholder translator convention keeps working, and every existing empty placeholder is fixed at once.
Empirical check that settles the question in seconds (run against `node_modules` i18next, initialized like the app):
```js
// lng: "fr", resources: { fr: { empty: "" }, en: { empty: "EnglishValue" } }
t("empty", "InlineDefault")
// returnEmptyString true (default) → "" ← blank UI
// returnEmptyString false → "EnglishValue"
```
## Why This Works
i18next resolution asks "does the key exist with a usable value?" — `returnEmptyString` defines whether `""` is usable. The default (`true`) is meant for apps where empty is a legitimate translation; in a placeholder-backfill workflow it's exactly wrong, because every placeholder is an intentional "not translated yet" marker.
## Prevention
- When adopting any `""`-placeholder catalog convention, set `returnEmptyString: false` in the same commit — the two are a package deal.
- Don't trust the inline-`t()`-default mental model; prove fallback behavior with a 5-line init script before relying on it.
- **Related catalog trap (hit twice in the same PR):** `pnpm i18n:extract` prunes keys whose usages it cannot see (CLI/TUI surfaces, dynamic keys) — it deleted live keys like `taskFields.*` and `common.cancel` from `en/app.json`. After running extract, semantically diff catalogs against the base ref (flatten both JSONs, assert zero removed/changed keys vs upstream, only intended additions) before committing. The content sanity test `packages/i18n/src/__tests__/config.test.ts` ("has real en content") exists because of this; prefer hand-adding keys + `i18n:sync`/`i18n:types` over trusting `i18n:extract` output wholesale.