Files
orca/config/scripts/verify-mobile-localization.test.mjs
T
OrcaWinandOrcaWin 12a7d4c13a feat(mobile): localization infrastructure and language setting (#35399)
* feat(mobile): add UI localization infrastructure

Mobile follows the phone's language (or an in-app choice under Settings > Appearance)
across the same 11 languages as desktop, switching live like the theme does.

- i18next with desktop's translate(key, fallback, options) call shape; catalogs in
  mobile/src/i18n/locales load lazily, English fallbacks stay inline
- Hermes lacks Intl.PluralRules: load the FormatJS polyfill with the 11 languages' rules
- OTA pages get the shell's language through an optional bridge init field
- iOS permission prompts localized; iOS and Android 13+ per-app language pickers
- verify:mobile-localization and a coverage ratchet run in lint, check:changed and CI

* fix(mobile): mount pages after the newest language request; tighten localization gates

- Wait for the latest page language activation, not a superseded one
- Keep literal fallbacks when a key also has a dynamic one
- Count headerTitle as visible copy; select the mobile check when the runtime catalog generator changes

* feat(mobile): repaint themed components on language changes

Components that read the theme already subscribe for repaint; a language switch
repaints them too, so labels built by helpers during render follow it. RPC
recordings load the real i18next and JSON catalogs.

* fix(mobile): scope the translate() gate to render bodies and memos; prune stale keys on sync

Handlers and effects may call translate() when they run. sync:mobile-localization now
removes keys no code references from every catalog.

* fix(mobile): use node: imports in the locale config plugin; pass uiLocale in bridge hook test

* chore(mobile): refresh the localization coverage baseline

* fix(mobile): follow render callbacks in the translate() gate; keep used plural siblings on prune

Also let anonymous default-exported screens count as components, flag any plural
family a locale started, and mark JSON modules as ES modules in the recording loader.

* fix(mobile): treat async and named local callbacks as handlers in the translate() gate

* chore(mobile): refresh the localization coverage baseline

* chore(mobile): refresh the localization coverage baseline

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 17:14:05 -07:00

55 lines
2.2 KiB
JavaScript

import { describe, expect, it } from 'vitest'
import {
collectIncompletePluralFamilies,
collectLiveSwitchViolations
} from './verify-mobile-localization.mjs'
describe('verify-mobile-localization', () => {
it('rejects translations that cannot follow a live language switch', () => {
const source = [
"const LABELS = [translate('a.b', 'Module scope')]",
'export function Row({ items }) {',
" const onPress = () => Alert.alert(translate('a.d', 'Handler runs at press time'))",
" useEffect(() => setTitle(translate('a.h', 'Effect')), [])",
" const label = useMemo(() => translate('a.g', 'Memo'), [])",
" return items.map((item) => <Text key={item}>{translate('a.c', 'Render callback')}</Text>)",
'}',
'export function Subscribed() {',
' const t = useMobileTranslation()',
" return <Pressable onPress={() => translate('a.i', 'Pressed')}>{t('a.e', 'Hook')}</Pressable>",
'}',
"export function rowLabel() { return translate('a.f', 'Helper') }",
"export default function () { return <Text>{translate('a.j', 'Screen')}</Text> }"
].join('\n')
expect(collectLiveSwitchViolations('mobile/src/row.tsx', source)).toEqual([
'mobile/src/row.tsx:1 translates at module scope; translate when rendering instead',
'mobile/src/row.tsx:5 calls translate() in a component or hook; use `const t = useMobileTranslation()`',
'mobile/src/row.tsx:6 calls translate() in a component or hook; use `const t = useMobileTranslation()`',
'mobile/src/row.tsx:13 calls translate() in a component or hook; use `const t = useMobileTranslation()`'
])
})
it('requires every CLDR category once a locale translates a plural family', () => {
const english = new Map([
['files_one', '{{count}} file'],
['files_other', '{{count}} files']
])
expect(
collectIncompletePluralFamilies(
english,
new Map([
['files_one', '{{count}} файл'],
['files_other', '{{count}} файла']
]),
'ru'
)
).toEqual(['files: missing _few, _many'])
expect(
collectIncompletePluralFamilies(english, new Map([['files_other', '{{count}} 件']]), 'ja')
).toEqual([])
})
})