mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
* 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.
* refactor(i18n): native i18next plurals at call sites, plural-aware extraction
Call sites branched on count === 1 and passed literal _one/_other keys,
which resolvePluralFamilyKey stripped again so CLDR could pick the form.
They now call translate('x', otherDefault, { count, defaultValue_one })
and i18next selects the form natively; catalog keys stay x_one/x_other.
- Converted 284 families with an AST codemod plus a few by hand; added
defaultValue_one to 21 base-key families that relied on en.json.
- Extraction runs with plurals enabled, so both English defaults are
checked for drift. A key that only fills {{count}} (no defaultValue_one)
stays singular in en.json.
- verify/sync:localization-catalog expand a native plural call into its
x_one/x_other English entries.
- Removed 22 base keys that a sibling plural family shadowed in every
catalog.
* fix(i18n): pick English plural defaults by English rules; require count for plural refs
* refactor(i18n): convert plural sites merged from main; treat key-as-default plural extraction as dynamic
* fix(i18n): keep an undefined pass-through count absent; only dotted key-as-default is dynamic
* test(i18n): update plural call expectations in two renderer tests
* refactor(i18n): convert a plural site merged from main
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
106 lines
3.9 KiB
JavaScript
106 lines
3.9 KiB
JavaScript
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
|
|
const PLURAL_KEY_RE = /^(.*)_(zero|one|two|few|many|other)$/
|
|
|
|
// Locales with more CLDR plural categories than English (ru few/many) source those forms from `_other`.
|
|
export function resolveLocaleSourceKey(enEntries, localeName, key) {
|
|
if (enEntries.has(key)) {
|
|
return key
|
|
}
|
|
const plural = PLURAL_KEY_RE.exec(key)
|
|
if (!plural || !localeName || !enEntries.has(`${plural[1]}_other`)) {
|
|
return undefined
|
|
}
|
|
return pluralCategories(localeName).includes(plural[2]) ? `${plural[1]}_other` : undefined
|
|
}
|
|
|
|
// translate('x', other, { count, defaultValue_one }) names the English catalog keys x_one and x_other.
|
|
export function pluralFamilyDefaults(ts, key, fallback, options) {
|
|
if (PLURAL_KEY_RE.test(key) || !options || !ts.isObjectLiteralExpression(options)) {
|
|
return undefined
|
|
}
|
|
const property = (name) => options.properties.find((entry) => entry.name?.getText() === name)
|
|
const one = property('defaultValue_one')
|
|
// Without defaultValue_one, count only fills {{count}} in a singular key; without count, i18next reads the base key.
|
|
if (!one || !property('count')) {
|
|
return undefined
|
|
}
|
|
const oneDefault =
|
|
ts.isPropertyAssignment(one) && ts.isStringLiteralLike(one.initializer)
|
|
? one.initializer.text
|
|
: undefined
|
|
return [
|
|
[`${key}_one`, oneDefault],
|
|
[`${key}_other`, fallback]
|
|
]
|
|
}
|
|
|
|
const COUNT_PLACEHOLDER_RE = /^\{\{\s*count\s*[,}]/
|
|
|
|
// i18next always passes count to plural forms, so a locale may add it where English omits it (ru 21 uses _one).
|
|
export function interpolationVariablesMatch(sourceKey, enVariables, localeVariables) {
|
|
if (enVariables.join('|') === localeVariables.join('|')) {
|
|
return true
|
|
}
|
|
const withoutCount = (variable) => !COUNT_PLACEHOLDER_RE.test(variable)
|
|
return (
|
|
PLURAL_KEY_RE.test(sourceKey) &&
|
|
!enVariables.some((variable) => COUNT_PLACEHOLDER_RE.test(variable)) &&
|
|
enVariables.join('|') === localeVariables.filter(withoutCount).join('|')
|
|
)
|
|
}
|
|
|
|
function pluralCategories(localeName) {
|
|
try {
|
|
return new Intl.PluralRules(localeName).resolvedOptions().pluralCategories
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
// Untranslated CLDR forms (fr/es/pt many, tr one) reuse the locale's `_other` instead of English; inserted beside it.
|
|
function withMissingPluralForms(catalog, categories, prefix = '', added = []) {
|
|
const filled = {}
|
|
for (const [key, value] of Object.entries(catalog)) {
|
|
const keyPath = prefix ? `${prefix}.${key}` : key
|
|
filled[key] =
|
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
? withMissingPluralForms(value, categories, keyPath, added).catalog
|
|
: value
|
|
if (typeof value !== 'string' || !key.endsWith('_other')) {
|
|
continue
|
|
}
|
|
const base = key.slice(0, -'other'.length)
|
|
for (const category of categories) {
|
|
if (!Object.hasOwn(catalog, `${base}${category}`)) {
|
|
filled[`${base}${category}`] = value
|
|
added.push(`${prefix ? `${prefix}.` : ''}${base}${category}`)
|
|
}
|
|
}
|
|
}
|
|
return { catalog: filled, added }
|
|
}
|
|
|
|
/** Verifies (or with fix, writes) the plural forms a desktop locale catalog needs; runtimes load catalogs as-is. */
|
|
export async function syncLocalePluralForms(catalogPath, catalog, fix) {
|
|
const fileName = path.basename(catalogPath)
|
|
const plural = withMissingPluralForms(catalog, pluralCategories(fileName.replace(/\.json$/, '')))
|
|
if (plural.added.length === 0) {
|
|
return { ok: true, catalog }
|
|
}
|
|
if (!fix) {
|
|
console.error(`${fileName} is missing CLDR plural forms that runtimes no longer fill:`)
|
|
console.error(
|
|
plural.added
|
|
.slice(0, 20)
|
|
.map((key) => `missing plural form: ${key}`)
|
|
.join('\n')
|
|
)
|
|
return { ok: false }
|
|
}
|
|
await fs.writeFile(catalogPath, `${JSON.stringify(plural.catalog, null, 2)}\n`, 'utf8')
|
|
console.log(`Added ${plural.added.length} plural form(s) to ${fileName} from \`_other\`.`)
|
|
return { ok: true, catalog: plural.catalog }
|
|
}
|