mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 09:07: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>
105 lines
3.3 KiB
JavaScript
105 lines
3.3 KiB
JavaScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
collectKeyLiterals,
|
|
compareExtraction,
|
|
findUnreferencedKeys
|
|
} from './verify-localization-extraction.mjs'
|
|
|
|
function sourceEvidence(sourceText) {
|
|
return collectKeyLiterals(sourceText, { literals: new Set(), dynamicPrefixes: new Set() })
|
|
}
|
|
|
|
describe('verify-localization-extraction', () => {
|
|
it('reports legacy drift without requiring a committed disposition database', () => {
|
|
const result = compareExtraction(
|
|
{ menu: { open: 'Open now {{name}}' } },
|
|
{ menu: { open: 'Open {{name}}', legacy: 'Legacy copy' } }
|
|
)
|
|
|
|
expect(result.orphans).toEqual(['menu.legacy'])
|
|
expect(result.fallbackDrift).toEqual(['menu.open'])
|
|
expect(result.placeholderMismatches).toEqual([])
|
|
})
|
|
|
|
it('reports dynamic defaults without treating empty extractor values as catalog copy', () => {
|
|
const result = compareExtraction(
|
|
{ menu: { dynamic: '' } },
|
|
{ menu: { dynamic: '{{count}} items' } }
|
|
)
|
|
|
|
expect(result.dynamicDefaults).toEqual(['menu.dynamic'])
|
|
expect(result.fallbackDrift).toEqual([])
|
|
expect(result.placeholderMismatches).toEqual([])
|
|
})
|
|
|
|
it('rejects undeclared keys even when their defaults are dynamic', () => {
|
|
const result = compareExtraction({ menu: { dynamic: '' } }, {})
|
|
|
|
expect(result.missingFromEnglish).toEqual(['menu.dynamic'])
|
|
})
|
|
|
|
it('rejects missing English declarations and incompatible placeholders', () => {
|
|
const result = compareExtraction(
|
|
{
|
|
menu: {
|
|
missing: 'Missing',
|
|
open: 'Open {{name}}'
|
|
}
|
|
},
|
|
{ menu: { open: 'Open {{path}}' } }
|
|
)
|
|
|
|
expect(result.missingFromEnglish).toEqual(['menu.missing'])
|
|
expect(result.placeholderMismatches).toEqual(['menu.open'])
|
|
})
|
|
|
|
it('tracks both plural defaults and keeps count-only keys singular', () => {
|
|
const result = compareExtraction(
|
|
{
|
|
files_one: '{{count}} file',
|
|
files_other: '{{count}} files',
|
|
selected_one: '{{count}} selected',
|
|
selected_other: '{{count}} selected'
|
|
},
|
|
{ files_one: 'One file', files_other: '{{count}} files', selected: '{{count}} selected' }
|
|
)
|
|
|
|
expect(result.missingFromEnglish).toEqual([])
|
|
expect(result.fallbackDrift).toEqual(['files_one'])
|
|
|
|
const dynamic = compareExtraction(
|
|
{ bg: { tasks_other: 'bg.tasks' } },
|
|
{ bg: { tasks_other: '{{count}} tasks' } }
|
|
)
|
|
expect(dynamic.dynamicDefaults).toEqual(['bg.tasks_other'])
|
|
})
|
|
|
|
it('keeps plural variants and data-structure keys that extraction cannot see', () => {
|
|
const evidence = sourceEvidence("const rows = [{ labelKey: 'rows.held' }]")
|
|
const extracted = new Map([['menu.count', '']])
|
|
|
|
expect(
|
|
findUnreferencedKeys(
|
|
['menu.count_one', 'menu.count_other', 'rows.held', 'rows.dead_one', 'menu.stale'],
|
|
extracted,
|
|
evidence
|
|
)
|
|
).toEqual(['rows.dead_one', 'menu.stale'])
|
|
})
|
|
|
|
it('keeps keys under a prefix that a translate call composes at runtime', () => {
|
|
const evidence = sourceEvidence(
|
|
"translate(`status.label.${state}`)\nt('menu.item.' + id)\nconst id = `rpc.${method}`"
|
|
)
|
|
|
|
expect(
|
|
findUnreferencedKeys(
|
|
['status.label.ready', 'menu.item.open', 'rpc.ping'],
|
|
new Map(),
|
|
evidence
|
|
)
|
|
).toEqual(['rpc.ping'])
|
|
})
|
|
})
|