@chipmobilesdk/rn-i18n
v0.1.1
Published
Reusable localization for ChipMobileSdk-family React Native apps: per-locale configuration, app-owned namespaced translations, typed keys, interpolation, pluralization, fallback chains, runtime locale switching with app-injected persistence, device-loc
Readme
@chipmobilesdk/rn-i18n
Reusable localization for ChipMobileSdk-family React Native apps: per-locale configuration, app-owned namespaced translations, typed keys, interpolation, pluralization, fallback chains, runtime locale switching with app-injected persistence, device-locale detection, locale-aware number/date/currency/unit formatting, and a Node CLI that extracts and synchronizes translation keys.
License:
UNLICENSED. Published publicly for use by the owner's applications; no open-source license is granted.
- Zero runtime dependencies. Formatting uses the
IntlAPIs built into Hermes; plural rules and unit conversion are package-owned. - No native code. Storage and (optionally) device-locale detection are injected by the consuming app.
- Public package. Published under the
@chipmobilesdknpm organization. - Version:
0.1.0 - Entrypoints:
@chipmobilesdk/rn-i18nand the Node-only@chipmobilesdk/rn-i18n/tooling
Installation
This package ships TypeScript sources and is consumed through the workspace or the public npm registry:
npm install @chipmobilesdk/rn-i18nHost frameworks are peer dependencies:
// peerDependencies
"react": ">=19",
"react-native": ">=0.85"Quick start
import { createI18n } from '@chipmobilesdk/rn-i18n';
import enUS from './locales/en-US.json';
import viVN from './locales/vi-VN.json';
import { storageAdapter } from './storage';
import type { AppI18nKey } from './keys.generated';
export const i18n = createI18n<AppI18nKey>({
primaryLocale: 'en-US',
locales: [
{
code: 'en-US',
displayName: 'English (US)',
direction: 'ltr',
fallbackChain: [],
defaultCurrency: 'USD',
defaultUnit: 'mile',
defaultDateFormat: { year: 'numeric', month: 'short', day: 'numeric' },
defaultNumberFormat: { maximumFractionDigits: 2 },
},
{
code: 'vi-VN',
displayName: 'Tiếng Việt',
direction: 'ltr',
fallbackChain: ['en-US'],
defaultCurrency: 'VND',
defaultUnit: 'kilometer',
defaultDateFormat: { day: '2-digit', month: '2-digit', year: 'numeric' },
defaultNumberFormat: { maximumFractionDigits: 2 },
},
],
resources: { 'en-US': enUS, 'vi-VN': viVN },
storage: storageAdapter,
});
await i18n.init(); // persisted -> device -> primary fallbackWrap the app and use the hooks:
import { I18nProvider, useTranslation, useLocale } from '@chipmobilesdk/rn-i18n';
<I18nProvider i18n={i18n}>
<App />
</I18nProvider>;
const { t } = useTranslation<AppI18nKey>();
const { locale, direction, supportedLocales, setLocale } = useLocale();
t('welcome.message', { name: 'Dung' });
t('cart.items', { count }, { count }); // plural variant by countPublic API
| Capability | Member |
|------------|--------|
| Setup + validation | createI18n(config), I18nConfigError |
| Translation lookup | i18n.t, useTranslation |
| Locale state + switching | i18n.getLocale, i18n.setLocale, useLocale |
| Locale metadata (RTL/LTR, defaults) | i18n.getSupportedLocales |
| Device-locale init | i18n.init, defaultDeviceLocaleProvider |
| Formatting | i18n.formatNumber/formatDate/formatCurrency/formatUnit |
| Unit conversion | i18n.convertUnit, convertUnit, getUnitSymbol, SUPPORTED_UNITS, UnitConversionError |
| Runtime-only declarations | i18n.registerRuntimeEntries |
| Persistence key | LOCALE_STORAGE_KEY |
| React | I18nProvider |
Public contract types also include the configuration, locale, translation,
formatting, unit, provider/adapter, and hook result types exported by
src/index.ts. Do not deep-import package internals. Node-only extraction and
synchronization APIs must be imported from @chipmobilesdk/rn-i18n/tooling.
Storage adapter (required for persistence)
The package never bundles a storage backend (FR-046/FR-047). Inject one:
import type { LocaleStorageAdapter } from '@chipmobilesdk/rn-i18n';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const storageAdapter: LocaleStorageAdapter = {
getItem: key => AsyncStorage.getItem(key),
setItem: (key, value) => AsyncStorage.setItem(key, value),
removeItem: key => AsyncStorage.removeItem(key),
};An MMKV-backed adapter (wrapping its synchronous API in Promise.resolve)
works equally well. If persistence is enabled without an adapter, createI18n
throws before any localized content is served.
Device-locale provider (optional)
The default reads Intl.DateTimeFormat().resolvedOptions().locale. Apps that
need richer detection can inject deviceLocale (e.g. backed by
react-native-localize); that dependency stays in the app, never the package.
Translations
App-owned JSON, organized by locale and namespace; nested dot-path keys,
{{name}} interpolation, and plural groups:
{
"welcome": { "message": "Welcome, {{name}}!" },
"cart": { "items": { "one": "{{count}} item", "other": "{{count}} items" } }
}Plural categories deliberately come from package-owned rules rather than
depending on Intl.PluralRules, which keeps behavior consistent across the
supported Hermes environments. Built-ins cover en (one/other), vi/ja
(other-only), and a default count === 1 -> one rule; supply
LocaleConfig.pluralRule for richer languages.
Extraction & synchronization CLI
Node-only, exposed via @chipmobilesdk/rn-i18n/tooling and a CLI. Add an app
config (i18n-sync.config.json) and a script:
{
"sourceGlobs": ["src/**/*.ts", "src/**/*.tsx"],
"patterns": [{ "regex": "[^\\w.]t\\(\\s*'(?<key>[A-Za-z0-9_.-]+)'" }],
"localesDir": "src/i18n/locales",
"locales": ["en-US", "vi-VN", "ja-JP"],
"runtimeEntriesFile": "src/i18n/runtime-keys.ts",
"generateKeysFile": "src/i18n/keys.generated.ts",
"keysTypeName": "AppI18nKey"
}// package.json
"scripts": {
"i18n:sync": "node --experimental-strip-types node_modules/@chipmobilesdk/rn-i18n/src/tooling/cli.ts --config i18n-sync.config.json"
}Running it scans the source tree, then per locale: creates missing files, adds
missing keys (with the placeholder policy), preserves existing translations,
removes stale keys, protects declared runtime-only keys, regenerates the typed
key contract, and prints a report. Exit codes: 0 success/warnings, 1
blocking findings (conflicts, malformed locale files), 2 invalid config.
Bind t() to the generated AppI18nKey so tsc --noEmit reports invalid or
stale key usage before release.
Runtime-only keys
Keys resolved only at runtime (declared in runtimeEntriesFile) use the same
lookup, fallback, interpolation, and missing-key behavior, and are never
removed by synchronization. Policy add writes the default text to missing
locales; report only lists them.
Compatibility
| Surface | Baseline |
|---------|----------|
| React / React Native | 19.x / 0.85+ (Hermes) |
| TypeScript | 5.8+ |
| Node (tooling) | >=22.11.0 |
| Platforms | Android, iOS (no native code) |
Validation
npm test # package + demo suites
npm run typecheck:i18n # tsc -p packages/rn-i18n/tsconfig.json --noEmit
npm run pack:i18n # npm pack --workspace @chipmobilesdk/rn-i18n --dry-run
npm run i18n:sync # extraction + synchronizationThe demo integration lives in src/screens/I18nDemoScreen.tsx and uses
src/i18n/ for setup, translations, persistence, and generated keys.
See CHANGELOG.md for release notes and adoption guidance.
