mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
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:
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user