Puts all visible text in react-intl catalogs - semantic keys, CLDR plurals, Intl polyfills, parity. Use when adding UI text or a hard-coded string, a message key, a plural, a translation or a language (i18n). Not for RTL (rtl-and-direction).
Installs into .claude/skills of the current project.
Are you the author of I18n Strings And Catalogs?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/zakariaf-i18n-strings-and-catalogs)
---
name: i18n-strings-and-catalogs
description: Puts all visible text in react-intl catalogs - semantic keys, CLDR plurals, Intl polyfills, parity. Use when adding UI text or a hard-coded string, a message key, a plural, a translation or a language (i18n). Not for RTL (rtl-and-direction).
---
# UI text, catalogs and translations
Every word a person sees comes from a catalog key that exists in every language the app ships, each
language follows its own CLDR plural and script rules, numbers format the same in Jest and on the
phone, and zero-dependency checkers prove the catalogs and the code keep it that way.
## Rules that must hold
1. **Render every visible or spoken string through `t()` or `<T>` from a catalog key.** No literals in JSX (text runs, also after a `{value}`; string, template or ternary children), in text or accessibility props (`accessibilityLabel`, `aria-label`, `alt`, `accessibilityValue.text`, `defaultValue`, custom `*Label`/`*Text`/`*Title`/`*Message` props), in navigation options (`options={{ title, tabBarBadge }}`, `navigation.setOptions`), in `Alert.alert` titles, messages and buttons, in screen-reader announcements (`announceForAccessibility`, `announce()`), toasts (`ToastAndroid.show`), action sheets (`ActionSheetIOS` options and title), notification content (`scheduleNotificationAsync`) or `Share.share`. Why: a literal is one language on every phone, for screen readers too (VoiceOver, TalkBack), and `{count} tasks left` glues a sentence with Latin digits.
2. **Keep one flat catalog per language, `src/i18n/catalogs/<tag>.json`, for exactly the languages in `src/i18n/languages.ts`, each tag in BCP 47 canonical casing (`en`, `pt-BR`, `zh-Hant`) and the file named exactly so.** The source language (`SOURCE_LANGUAGE`, usually `en`) is authoritative; every catalog has the same keys, sorted. No runtime fallback to the source: a missing key reaches `onError`. Why: stable diffs, a missing text fails a check, not a person, and `pt-br.json` loads on a Mac but not on Linux CI or EAS Build.
3. **Name keys `<area>.<element>[.<variant>]` (2-5 kebab segments) and write them as literals or in `as const satisfies readonly MessageKey[]` tables; never build or cast a key.** Why: `MessageKey = keyof typeof en` and the checkers only see literal keys; a cast hides a typo; semantic keys survive rewording.
4. **Type every number:** `{n, number}`, a skeleton (`{rate, number, ::percent}`) or a plural `#`. A plain `{x}` is only free text named `*Name`/`*Text`. Why: a plain argument prints `String(value)`, Latin digits in every language.
5. **Write every counted noun as an ICU plural with exactly the CLDR categories of each language** (`new Intl.PluralRules(lang).resolvedOptions().pluralCategories`: en one/other, ar zero/one/two/few/many/other, ja other), and the same `=N` branches in every language. Never branch on `n === 1` in code. Why: Arabic needs six forms, Persian puts 0 in `one`, Russian puts 0 in `many`.
6. **One whole sentence per key, in the language's own script rules:** no glued fragments, no literal digits, no bidi controls, Arabic-script punctuation (`،` `؛` `؟`) in Arabic-script languages, `ی`/`ک` in Persian, Sorani and Urdu. Why: word order differs per language; look-alike characters break search, sorting and fonts.
7. **Let `t()` isolate free text (FSI…PDI) and never isolate twice:** strip the inner isolates (`stripIsolates`) before a formatted text becomes a value of another message. Why: a Latin name inside an Arabic sentence must not reorder it, and nested isolates make CoreText run a date the wrong way.
8. **Force the FormatJS polyfills (Locale, PluralRules, NumberFormat) with data for exactly `LANGUAGES`, as the first import of the app entry `index.ts` (then `expo-router/entry`) and in every Jest `setupFiles`; a regional language imports the file each package ships, found by truncation fallback (the tag, then the tag minus its last subtag: `pt-BR` -> pluralrules `pt.js`), which is how the FormatJS polyfills and Node's ICU find plural data.** `Intl.DateTimeFormat` (rule 10), `RelativeTimeFormat`, `ListFormat`, `DisplayNames` and `Segmenter` run only in `src/i18n/`, each on its own forced block; code never calls `Intl.PluralRules`. Why: Hermes has no `PluralRules`, `Locale` or those last four (`not a constructor` on the phone while Jest passes) and ignores `-u-nu-`; forcing makes Jest identical to the phone.
9. **Carry the numbering system in the locale tag** (`localeTagFor(language, digits)`: `ar-u-nu-arab`, `ar-u-nu-latn`) and use the same tag for messages and stand-alone numbers; never `new Intl.NumberFormat('ar')`. Why: CLDR defaults move (CLDR 48 switched `ar` to Latin digits) and the person may pick Latin digits.
10. **Format dates and times only at the edge, in `src/i18n/`, polyfilled, with an explicit calendar, an explicit time zone and the locale tag's digits.** `Intl.DateTimeFormat` lives there on the forced `@formatjs/intl-datetimeformat` (data per language plus one time-zone file; it defaults to UTC, so pass `timeZone`). The template's day-month labels use CLDR month names copied into the catalogs instead, which needs no DateTimeFormat polyfill (about 1.2 MB raw for two languages); a time of day or other shapes take the Intl path ([examples/intl-date-time.md](examples/intl-date-time.md)); a relative time ("3 days ago") is a catalog plural. No `{x, date}`/`{x, time}` in catalogs, no `toLocale*String` or `Intl.DateTimeFormat` elsewhere. Why: native Hermes picks the device calendar and ignores the digit choice.
11. **Import `react-intl` only inside `src/i18n/`; send `onError` to the error log in the app and throw in tests.** Why: one text API (`useT`, `<T>`), and a missing message fails the test run.
12. **Resolve the language as saved choice, then each device locale's full tag, language-Script, then language (explicit aliases first), then the source; declare `supportedLocales` = `LANGUAGES` (`readAppLanguages`) in the expo-localization plugin.** Autonyms are code, never catalog text. Why: iOS Settings and Android 13+ per-app language list only declared languages; guessed mappings pick the wrong script.
13. **Keep permission purpose strings and the app name in the catalogs (`<area>.<topic>.usage-description`, `app.name`) as plain text.** app.config builds `locales` from them (`nativeLocales`, keeping the variant's label), lists every purpose key it sets in `NATIVE_TEXT_KEYS`, gives a plugin its base text with `sourceText`, and holds no literal purpose string. Why: iOS prints them from `InfoPlist.strings`, where nothing formats ICU; a literal is one language for everyone and a second copy drifts.
14. **Translate source first, then every language with the same placeholders; drafted translations go on the non-blocking review queue (`review-sheet.ts`); approved copy is synced (`sync-copy.mjs`), never retyped; developer-only keys stay untranslated only through `untranslatedPrefixes`.** Why: drafts need a fluent reader without stopping work, and retyping loses ’ – and ZWNJ.
## Workflow
1. **First time (the app has no i18n layer).** Read [references/runtime-and-provider.md](references/runtime-and-provider.md). Install the packages its table names (`react-intl` and the four `@formatjs/intl-*` polyfills exact, `intl-datetimeformat` only for rule 10's Intl path; `npx expo install expo-localization`; dev: `@formatjs/cli`, `@formatjs/icu-messageformat-parser`). Copy `templates/src/i18n/` (catalogs, polyfills, languages, digits, bidi, `t()`, `<T>`, provider, dates, language resolution, `translate-now.ts` for text outside the provider, each with its test), `templates/src/core/dates/date-key.ts` if the app lacks it, `templates/index.ts`, `templates/test/i18n/`, `templates/scripts/i18n/` and `templates/contracts/i18n.json`. In `package.json` set `"main": "index.ts"` and the script `"i18n:verify": "node scripts/i18n/verify-catalogs.ts"` (quality-gates' `npm run verify`, which CI runs, calls it). Set `LANGUAGES`, `SOURCE_LANGUAGE`, autonyms and directions in `languages.ts`, the digits rows in `digits.ts`, one locale-data import per language in `intl-polyfills.ts` and one catalog per language. An app that keeps the i18n folder elsewhere sets `I18N_DIR` in `scripts/i18n/app-languages.ts` and `"i18nDir"` in `rn-skills.config.json` together. Render `<I18nProvider language digits onError>` in the root layout `src/app/_layout.tsx` (language from `resolveLanguage(saved, getLocales())`), with design-system-structure's `<TextLocaleProvider value={{ language, direction: directionOf(language) }}>` inside it, and title screens in a group layout under it, `src/app/(app)/_layout.tsx` (`useT()` throws outside the provider). Text shown outside the provider (the root crash fallback) uses `createTranslateNow(onError)` from `translate-now.ts`: no React context, the language and digits the provider showed last (the template catalogs carry its `crash.title`, `crash.body` and `crash.restart` keys). Wrap app.config.ts's result in `withI18n` (runtime reference, "Declaring the languages"): `locales` from the catalogs with the variant's name, expo-localization's `supportedLocales` from `readAppLanguages(projectRoot)`.
2. **Adding a language.** Add it to `LANGUAGES`, write `<lang>.json` with every key (source first), add its two locale-data imports and its `digits.ts` row, `messages.ts` entry, autonym and direction (`supportedLocales` follows `LANGUAGES` through `readAppLanguages`). `check-catalogs.mjs` then tells you every plural that lacks one of its CLDR categories. A regional or script language (`pt-BR`, `zh-Hant`) imports the locale-data file each package ships (`check-i18n-code.mjs` names it) and may need an alias for how phones report it (Android: `zh-TW`).
3. **Building or changing a screen.** Look for existing keys first. Use keys as literals: `<T id="tasks.summary.open" values={{ openCount }} />`, or `t('…')` in the model hook for props, accessibility labels and navigation options. Components receive translated strings; they never build them. An id that arrives at runtime (a game's message id, a server code mapped to a key) goes through `translateRuntimeMessage(t, { id, values }, onError)` from `templates/src/i18n/runtime-message.ts`, which checks it against the source catalog instead of casting it.
4. **Text the catalogs lack.** Read [references/catalogs-keys-and-icu.md](references/catalogs-keys-and-icu.md) and [references/translation-workflow.md](references/translation-workflow.md), and follow [examples/add-a-counted-message.md](examples/add-a-counted-message.md): source text first, then every language with the same placeholders and each language's plural categories, keys sorted, drafts on the review queue.
5. **Approved copy (when design or marketing owns the texts).** Keep it in `contracts/copy-source.json` (shape: [examples/copy-source.json](examples/copy-source.json)); look up with `node ${CLAUDE_SKILL_DIR}/scripts/sync-copy.mjs keys . --screen <id> --lang all`, write with `apply . --screen <id>`; a wording change goes into the copy source first.
6. **Permission texts and the app name.** Add `<area>.<topic>.usage-description` keys (plain text) to every catalog and list each in `withI18n`'s `NATIVE_TEXT_KEYS` (`NSCameraUsageDescription: 'permissions.camera.usage-description'`); `nativeLocales` builds app.config `locales` from the catalogs. A config plugin that writes the base text gets it from the catalogs too: `['expo-image-picker', { cameraPermission: sourceText(projectRoot, 'permissions.camera.usage-description'), photosPermission: false, microphonePermission: false }]` (`false` for each permission it never asks: an installed plugin writes an English default otherwise). When the permission flow itself is the task, hand off to `permissions-and-system-prompts`.
7. **Tests.** Keep the template tests green. A message with a count or a placeholder gets a `t()` assertion in each language whose grammar differs (Arabic: 0, 1, 2, 3, 11, 100). Component tests render through unit-and-component-tests' `renderWithProviders`, whose `test/test-app.tsx` mounts `I18nProvider` (real catalogs, a throwing `onError`), not a bare `IntlProvider`: `<T>` and `useT()` need its `TContext` (runtime reference, "Jest setup").
8. **Run the checks** from the repo root, fix every `FAIL` line (each names the file, the rule and the fix) and rerun until each prints `RESULT: PASS`:
- `node ${CLAUDE_SKILL_DIR}/scripts/check-catalogs.mjs .` (rules L1-L13, parity P1-P5, files F1)
- `node ${CLAUDE_SKILL_DIR}/scripts/check-i18n-code.mjs .` (literals, plural forms picked in code, keys, casts, react-intl, dates, Intl APIs, isolates, wiring)
- `node ${CLAUDE_SKILL_DIR}/scripts/sync-copy.mjs check . --screen <id>` when the app has a copy source (without one it prints `NOT APPLICABLE`)
Look up a rule id or a bad-input exit in [references/checkers-and-contracts.md](references/checkers-and-contracts.md) (every option, contract key and SKIP line). When multi-package, add `--app-dir <workspace>` (for example `apps/mobile`) to each. Then `npx jest src/i18n scripts/i18n test/i18n --ci` and `npm run i18n:verify` (the in-repo gate CI runs without the skill: FormatJS verify plus the same catalog linter).
9. **The review queue (never blocking).** Run `node scripts/i18n/review-sheet.ts`: it writes `reports/i18n/review-<lang>.csv` for each `reviewLanguages` entry and prints `REVIEW PENDING (not blocking): …`. Copy that line into the report with the questions from the translation-workflow reference and carry on. When corrections arrive, apply them (copy source first), rerun the checks, then `node scripts/i18n/review-sheet.ts --mark-reviewed <lang> --date YYYY-MM-DD`.
## Definition of done
- [ ] No visible or spoken string (text prop, accessibility label or value, navigation option, alert, announcement, toast, action sheet) is a literal, and no plural form is picked in code; every key is a literal or comes from a typed table, never a cast.
- [ ] Every language in `LANGUAGES` has a catalog with the source's keys, sorted; plurals carry each language's CLDR categories and the same `=N` branches.
- [ ] `index.ts` imports `@/i18n/intl-polyfills` first and `expo-router/entry` last, `package.json` `main` is `index.ts`, and Jest `setupFiles` loads the polyfills; every locale-data import exists in the installed package, and every Intl API Hermes lacks runs only in `src/i18n/` on its forced block.
- [ ] `package.json` has `"i18n:verify"` and `npm run i18n:verify` prints `0 catalog folder(s) failing` (`@formatjs/cli` installed); each problem it prints reads `<file>:<line> [<rule id> <name>] <key>: <message>`, like the checker.
- [ ] Permission texts and the app name reach app.config `locales` from the catalogs, a development build keeps its label (`Tasks (Dev)` in every language), app.config holds no literal purpose string, and `supportedLocales` equals `LANGUAGES` (`APP_VARIANT=development npx expo config --json` shows all of it).
- [ ] The i18n Jest tests pass; drafted texts are listed in the report as review pending, and nothing waits for the review.
- [ ] A passing run may print only these SKIP lines, while the app is still being set up: `SKIP package.json [polyfill-first] not yet created (expo-project-bootstrap creates it)`, `SKIP package.json [verify-script] not yet created (expo-project-bootstrap creates it)`, `SKIP jest.config.js [jest-polyfills] not yet created (unit-and-component-tests creates it)`, `SKIP app.config.ts [supported-locales] not yet created (expo-project-bootstrap creates it)`, `SKIP app.config.ts [system-text-locales] not yet created (expo-project-bootstrap creates it)`.
- [ ] With a copy source: `node ${CLAUDE_SKILL_DIR}/scripts/sync-copy.mjs check . --screen <id>` prints `RESULT: PASS` for each screen built.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-catalogs.mjs .` prints `RESULT: PASS`.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-i18n-code.mjs .` prints `RESULT: PASS`.
## Anti-patterns
- **Hard-coding English "for now".** It ships. Add the key in every language now; a missing translation fails the checks on purpose.
- **Gluing sentences:** `t('a') + ' ' + count + ' ' + t('b')`. Word order differs per language and the digits stay Latin. Write one message with placeholders.
- **`count === 1 ? 'task' : 'tasks'` (or `t('a')`/`t('b')` in the branches), or a plural with only `one` and `other` in Arabic, Polish or Russian.** Use an ICU plural with the language's own categories; the checkers name the branch (`count-branch`) and the missing categories.
- **Building or casting keys:** `` t(`date.month-short.${m}`) ``, `t(code as MessageKey)`. Use a typed table of literal keys, or a `switch` that returns literal keys.
- **`toLocaleDateString()` or `Intl.DateTimeFormat` in a screen.** The device calendar and digits leak in (native Hermes formats `fa` dates in the Solar Hijri calendar on iOS); use `format-date.ts`, or a formatter in `src/i18n/` ([examples/intl-date-time.md](examples/intl-date-time.md)).
- **`Intl.DateTimeFormat` under the polyfill without `timeZone`.** The polyfill does not know the device zone, so every time prints in UTC; pass `getCalendars()[0]?.timeZone`.
- **Relying on CLDR's default digits** (`new Intl.NumberFormat('ar')`). The default changed between CLDR releases; pass `localeTagFor(language, digits)`.
- **Importing the polyfills from the root layout.** With `main: expo-router/entry`, route modules load first; the custom entry `index.ts` is the documented place.
- **A second i18n library, or locale data "just in case".** Every locale file is startup weight; `LANGUAGES` is the list.
- **Translating autonyms or developer-only screens.** Autonyms are code; developer screens go under `untranslatedPrefixes`, never ad hoc.
- **An ICU argument in a permission text** (`"{appName} uses the camera"`). iOS shows the braces; the dialog title already names the app.
- **A literal purpose string in app.config** (`cameraPermission: 'Allow Tasks to take photos'`, `infoPlist.NSMicrophoneUsageDescription: '…'`). Every language sees that one text, and the reviewer never does; read it with `sourceText` and list the key in `NATIVE_TEXT_KEYS`. A plugin's own default (`Allow $(PRODUCT_NAME) to …`) is the same: prebuild applies an installed expo-image-picker or expo-camera even unlisted.
- **One app name for every variant.** `nativeLocales` without `variantName` writes the store name into every declared language, so the dev and preview builds look like production on those phones.
- **Waiting for the reviewer, or forgetting the queue.** The review never blocks, but every report lists what is pending.
- **Retyping an approved text into a catalog.** Typographic apostrophes, dashes and ZWNJ get lost; `sync-copy.mjs apply` copies byte for byte.
## Files in this skill
| File | What it is | Read/run when |
|---|---|---|
| [references/catalogs-keys-and-icu.md](references/catalogs-keys-and-icu.md) | Catalog files, key grammar, placeholders, plurals per CLDR, bidi isolation, system texts, untranslated keys, dates, script rules, every catalog rule, what FormatJS verify misses | Workflow steps 2-4, and when a catalog check fails |
| [references/runtime-and-provider.md](references/runtime-and-provider.md) | Packages and versions, why the polyfills are forced (the DateTimeFormat block, the Intl APIs Hermes lacks), regional and script languages (locale-data fallback, device matching), a moved i18n folder, the custom entry, the i18n folder and `i18n:verify`, language resolution and expo-localization on iOS and Android, `withI18n` (variant label, plugin base texts), locale tags, `t()`/`<T>`/provider and where titled screens go, Jest, device probes, multi-package | Workflow steps 1 and 6, and when changing runtime i18n code |
| [references/translation-workflow.md](references/translation-workflow.md) | Writing a new message, the copy source and `sync-copy.mjs`, glossary and tone, the review queue, reviewer questions, store listings | Workflow steps 4, 5 and 9 |
| [references/checkers-and-contracts.md](references/checkers-and-contracts.md) | Every checker option, `contracts/i18n.json` and `contracts/copy-source.json` keys, `rn-skills.config.json` keys, SKIP lines, multi-package setup | Workflow step 8: a FAIL rule, a bad-input exit, or an app layout that differs |
| [examples/add-a-counted-message.md](examples/add-a-counted-message.md) | A new plural message in en, de, fa and ar, from key to test to checks | Workflow step 4 |
| [examples/copy-source.json](examples/copy-source.json) | The shape of `contracts/copy-source.json` | Workflow step 5 |
| [examples/intl-date-time.md](examples/intl-date-time.md) | Times of day with `Intl.DateTimeFormat` in `src/i18n/`: the polyfill block, explicit calendar and time zone, verified outputs and test | When a date or time needs more than catalog month names (rule 10) |
| `templates/src/i18n/` | The runtime: catalogs (en, ar); `intl-polyfills.ts`, `intl-status.ts` (+ test), `languages.ts`, `digits.ts`, `bidi.ts`, `language-context.tsx`, `translate-now.ts` (+ test: t() without React context, for the crash fallback) (synced from the library: change them there); `messages.ts`, `create-t.ts`, `t-context.ts`, `t-bridge.tsx`, `i18n-provider.tsx`, `t.tsx`, `format-date.ts`, `resolve-language.ts`, with tests; `runtime-message.ts` (+ test: `isMessageKey`, `translateRuntimeMessage` for ids that arrive at runtime) | Workflow steps 1 and 3; copy to `src/i18n/` |
| `templates/src/core/dates/date-key.ts` | Civil-date keys `format-date.ts` reads (synced from seeded-determinism-and-golden-vectors) | Workflow step 1, only if the app lacks it |
| `templates/index.ts` | The custom app entry: polyfills first, `expo-router/entry` last | Workflow step 1 |
| `templates/test/i18n/` | Guardrail test that the entry imports the polyfills first | Workflow step 1 |
| `templates/scripts/i18n/` | `verify-catalogs.ts` (`npm run i18n:verify`: FormatJS verify + the linter `catalog-lint*.ts`, `icu-walk.ts`, which prints the checker's rule ids, lines and messages for L1-L13, P4, P5 and F1; FormatJS reports missing and extra keys and placeholder types itself), `native-locales.ts` (app.config `locales`, `sourceText`), `app-languages.ts` (`LANGUAGES` for Node tooling, read without importing app code), `i18n-contract.ts` (finds `contracts/` at the repo root), `review-sheet.ts` (the review queue), with tests | Workflow steps 1, 6 and 9 |
| `templates/contracts/i18n.json` | The app's i18n contract: untranslated prefixes, review languages, isolated formatters, package catalogs | Workflow step 1 |
| `scripts/check-catalogs.mjs` | Checks every catalog: L1-L13, parity P1-P5, files F1 (BCP 47 casing, exact file names), plural categories and scripts from Node's ICU | Workflow step 8, after every catalog change |
| `scripts/check-i18n-code.mjs` | Checks source for literal text (JSX, props, navigation, alerts, announcements, toasts, action sheets, notifications, share sheets), plural forms picked in code, built, cast or unknown keys (also the keys app.config names), react-intl, date, number and Hermes-missing Intl APIs, nested isolates, runtime files, polyfill data (per installed file) and order, Jest setup, the `i18n:verify` script, the tooling's i18n folder, declared languages and permission texts in app.config | Workflow steps 1 and 8 |
| `scripts/sync-copy.mjs` | Looks up (`keys`), writes (`apply`) and verifies (`check`) approved copy against the catalogs | Workflow steps 5 and 8 |
| `scripts/lib/catalog-rules.mjs` | The catalog rule implementations and messages | Read only when changing a rule |
| `scripts/lib/i18n-config.mjs` | Reads `languages.ts`, the contract and the catalog folders for every checker | Read only when changing a checker |
| `scripts/lib/icu-parse.mjs` | Dependency-free ICU MessageFormat parser (FormatJS element shapes) | Read only when changing a checker |
| `scripts/lib/jsx-text.mjs` | Finds the text a JSX element renders (runs, literal, built and ternary children) | Read only when changing a checker |
| `scripts/lib/app-config-rules.mjs` | Literal purpose strings, uncovered purpose keys, plugin option to Info.plist key table, `supportedLocales` | Read only when changing a checker |
| `scripts/lib/source-scan.mjs` | JSX, call and import scanning (synced from the library) | Never by hand |
| `scripts/lib/ts-literal.mjs` | Reads `LANGUAGES` as a literal (synced from the library) | Never by hand |
| `scripts/check-lib.mjs` | Shared script helper (synced from the library) | Never by hand |
| `scripts/selftest.mjs` | Proves every checker passes its good fixtures and catches each planted bug | After changing a script or fixture |
| `assets/i18n-contract.schema.json` | JSON Schema of `contracts/i18n.json` | When editing the contract |
| `assets/copy-source.schema.json` | JSON Schema of `contracts/copy-source.json` | When creating a copy source |
| `assets/shared.json` | The shared files this skill copies in | When adding a shared file |
| `tests/bases/` | Base trees the fixtures build on (four-language catalogs, a small app, package catalogs, a copy source) | When adding a fixture |
| `tests/fixtures/` | Good, planted-bad, pass and error cases, one folder per checker suite | When adding a rule |
## Related skills
- `rtl-and-direction` - layout direction, mirroring, digits outside messages, bidi in layout, script fonts.
- `error-handling-typed-results` - the root crash fallback that renders its copy with `createTranslateNow`.
- `accessibility-as-code` - roles, labels and hints (their text comes from here), text scaling.
- `design-system-structure` - the `AppText` that `<T>` renders and the `TextLocaleProvider` the root layout mounts.
- `settings-and-preferences` - the saved language and digits settings (`null` = follow the device).
- `permissions-and-system-prompts` - when and how to ask for a permission whose purpose text lives here.
- `naming-conventions` - naming beyond message keys; its `i18n-key-namespace` rule checks a key's first segment against feature folders and `contracts/naming.json` `keyAreas`.
- `dates-streaks-and-statistics` - date keys, durations and statistics messages.
- `value-objects-money-and-units` - money and quantity formatting (`formatMoney`, `useLocaleTag`), stricter than `number-format-api`.
- `eas-app-stores` (Expo) - localized store listings.