From 9843fb9fcb1b63e9cc282b24bf0841fca8453caa Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 5 Jun 2026 14:23:27 -0700 Subject: [PATCH] docs(solutions): i18next empty-placeholder blank-render learning + Translation Placeholder concept --- CONCEPTS.md | 3 + ...-empty-locale-placeholders-render-blank.md | 65 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 docs/solutions/ui-bugs/i18n-empty-locale-placeholders-render-blank.md diff --git a/CONCEPTS.md b/CONCEPTS.md index 4602719bf1..0fe16933cd 100644 --- a/CONCEPTS.md +++ b/CONCEPTS.md @@ -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. diff --git a/docs/solutions/ui-bugs/i18n-empty-locale-placeholders-render-blank.md b/docs/solutions/ui-bugs/i18n-empty-locale-placeholders-render-blank.md new file mode 100644 index 0000000000..ecd436d16e --- /dev/null +++ b/docs/solutions/ui-bugs/i18n-empty-locale-placeholders-render-blank.md @@ -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.