refactor(i18n): share one i18next setup between main and renderer (#35621)

* perf(i18n): load built-in catalogs once, without copies or a runtime plural fill

Each catalog load parsed the JSON, rebuilt it to fill CLDR plural forms
from _other, then deep-copied and deep-merged it into the store (about
26 ms after parsing per 1.3 MB catalog), and concurrent loads of one
locale fetched it twice.

- sync:localization-catalog now commits the plural forms a locale's
  CLDR rules need beside each _other (fr/es/pt many, tr one, a few
  de/ru), and verify:localization-catalog fails when one is missing.
  Plural forms may include {{count}} where English omits it.
- Renderer and main load built-in catalogs as-is; plugin packs keep the
  runtime fill.
- Renderer catalog requests are shared per locale; a timed-out request
  is dropped so a retry fetches again. Bundles are added with skipCopy.

* fix(i18n): require {{count}} in plural forms when English shows it

* refactor(i18n): share one i18next setup between main and renderer

- createOrcaI18n (src/shared/orca-i18n-instance.ts) owns the backend,
  init options, plugin-pack registration, resource-language resolution
  and the translate wrapper; main and renderer inject only how a
  built-in catalog loads (dynamic import vs. fetch) and extra modules.
- Remove setRendererUiLanguage (tests only); tests load then switch.
- resolveUiLocale maps built-ins by lookup instead of an if-chain, and
  catalog URLs/loaders are direct map lookups.
- Drop <I18nextProvider>: initReactI18next already binds the instance.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-28 20:21:12 -07:00
committed by GitHub
co-authored by OrcaWin
parent 73c6b6d2ef
commit c10fbcb0d2
14 changed files with 187 additions and 263 deletions
+93
View File
@@ -0,0 +1,93 @@
import i18next, {
type BackendModule,
type i18n as I18nInstance,
type InitOptions,
type Module,
type ReadCallback,
type TOptions
} from 'i18next'
import { resolvePluralFamilyKey } from './i18n-plural-family'
import { isPseudoLocalizationLocale, pseudoLocalizeString } from './pseudo-localization'
import { DEFAULT_UI_LOCALE } from './ui-locale'
import type { PluginLanguagePackRegistration } from './plugins/plugin-language-pack-artifact'
import { replacePluginLanguagePackResources } from './plugins/plugin-language-pack-i18n-resources'
export type OrcaI18n = {
i18n: I18nInstance
translate: (key: string, fallback: string, options?: TOptions) => string
/** Returns whether any pack's resources changed. */
setPluginLanguagePacks: (packs: readonly PluginLanguagePackRegistration[]) => boolean
getPluginLanguagePacks: () => readonly PluginLanguagePackRegistration[]
/** Maps a resolved UI locale to its i18next resource language; an unknown pack falls back to English. */
resolveResourceLanguage: (locale: string) => string
}
/** Main and renderer share one setup; only how a built-in catalog loads differs. */
export function createOrcaI18n(options: {
/** Undefined for English and unknown locales, which use caller fallbacks. */
loadCatalog: (language: string) => Promise<Record<string, unknown>> | undefined
englishResources: Record<string, unknown>
modules?: Module[]
initOptions?: InitOptions
}): OrcaI18n {
const i18n = i18next.createInstance()
let pluginLanguagePacks: readonly PluginLanguagePackRegistration[] = []
const backend: BackendModule = {
type: 'backend',
init: () => {},
read: (language: string, _namespace: string, callback: ReadCallback) => {
const load = options.loadCatalog(language)
if (!load) {
callback(null, false)
return
}
load.then(
(catalog) => callback(null, catalog),
(error) => callback(error instanceof Error ? error : new Error(String(error)), false)
)
}
}
i18n.use(backend)
for (const module of options.modules ?? []) {
i18n.use(module)
}
// In-memory resources make init synchronous, so callers can translate at module load.
void i18n.init({
fallbackLng: DEFAULT_UI_LOCALE,
lng: DEFAULT_UI_LOCALE,
// Default 'all' expands zh-Hant to [zh-Hant, zh, en], so missing keys would render Simplified.
load: 'currentOnly',
// Serve eager English from resources while the backend lazily supplies other languages.
partialBundledLanguages: true,
resources: { [DEFAULT_UI_LOCALE]: { translation: options.englishResources } },
// An empty catalog value falls back to English instead of rendering blank.
returnEmptyString: false,
interpolation: { escapeValue: false },
...options.initOptions
})
return {
i18n,
translate: (key, fallback, translateOptions) => {
const value = i18n.t(resolvePluralFamilyKey(key, translateOptions), {
defaultValue: fallback,
...translateOptions
})
return isPseudoLocalizationLocale(i18n.language) ? pseudoLocalizeString(value) : value
},
setPluginLanguagePacks: (packs) => {
if (packs === pluginLanguagePacks) {
return false
}
const changed = replacePluginLanguagePackResources(i18n, pluginLanguagePacks, packs)
pluginLanguagePacks = packs
return changed.size > 0
},
getPluginLanguagePacks: () => pluginLanguagePacks,
resolveResourceLanguage: (locale) =>
pluginLanguagePacks.find((pack) => pack.id === locale)?.resourceLanguage ??
(locale.startsWith('plugin:') ? DEFAULT_UI_LOCALE : locale)
}
}
+6 -50
View File
@@ -1,19 +1,4 @@
import {
UI_LANGUAGE_CHINESE,
UI_LANGUAGE_CHINESE_TRADITIONAL,
UI_LANGUAGE_ENGLISH,
UI_LANGUAGE_FRENCH,
UI_LANGUAGE_GERMAN,
UI_LANGUAGE_JAPANESE,
UI_LANGUAGE_KOREAN,
UI_LANGUAGE_PORTUGUESE,
UI_LANGUAGE_RUSSIAN,
UI_LANGUAGE_SPANISH,
UI_LANGUAGE_SYSTEM,
UI_LANGUAGE_TURKISH,
isPluginUiLanguage,
type UiLanguage
} from './ui-language'
import { UI_LANGUAGE_SYSTEM, isPluginUiLanguage, type UiLanguage } from './ui-language'
export const SUPPORTED_UI_LOCALES = [
'en',
@@ -146,40 +131,11 @@ export function resolveUiLocale(
if (isPluginUiLanguage(language)) {
return language
}
if (language === UI_LANGUAGE_ENGLISH) {
return DEFAULT_UI_LOCALE
}
if (language === UI_LANGUAGE_CHINESE) {
return 'zh'
}
if (language === UI_LANGUAGE_CHINESE_TRADITIONAL) {
return 'zh-Hant'
}
if (language === UI_LANGUAGE_KOREAN) {
return 'ko'
}
if (language === UI_LANGUAGE_JAPANESE) {
return 'ja'
}
if (language === UI_LANGUAGE_SPANISH) {
return 'es'
}
if (language === UI_LANGUAGE_FRENCH) {
return 'fr'
}
if (language === UI_LANGUAGE_PORTUGUESE) {
return 'pt'
}
if (language === UI_LANGUAGE_RUSSIAN) {
return 'ru'
}
if (language === UI_LANGUAGE_TURKISH) {
return 'tr'
}
if (language === UI_LANGUAGE_GERMAN) {
return 'de'
}
return normalizeSupportedUiLocale(systemLocale)
// Built-in UI language values are their own locale codes; only 'system' needs detection.
return (
SUPPORTED_UI_LOCALES.find((locale) => locale === language) ??
normalizeSupportedUiLocale(systemLocale)
)
}
export function getRendererSystemLocale(): string {