@chipmobilesdk/rn-i18n 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -57
- package/README.md +218 -218
- package/package.json +46 -49
- package/skills/sdk-rn-i18n/SKILL.md +61 -62
- package/src/config/types.ts +144 -152
- package/src/config/validateConfig.ts +156 -174
- package/src/format/currency.ts +27 -27
- package/src/format/date.ts +26 -26
- package/src/format/invalid.ts +13 -16
- package/src/format/number.ts +23 -25
- package/src/format/units.ts +141 -151
- package/src/i18n.ts +196 -222
- package/src/index.ts +50 -50
- package/src/locale/activeLocale.ts +46 -45
- package/src/locale/deviceLocale.ts +50 -54
- package/src/locale/persistence.ts +31 -37
- package/src/react/I18nProvider.tsx +31 -35
- package/src/react/hooks.ts +57 -61
- package/src/tooling/cli.ts +76 -78
- package/src/tooling/generateKeys.ts +62 -68
- package/src/tooling/index.ts +27 -32
- package/src/tooling/report.ts +109 -117
- package/src/tooling/runner.ts +111 -126
- package/src/tooling/scan.ts +193 -210
- package/src/tooling/sync.ts +193 -198
- package/src/tooling/types.ts +119 -119
- package/src/translation/interpolate.ts +23 -23
- package/src/translation/plurals.ts +62 -69
- package/src/translation/resolver.ts +140 -146
- package/src/translation/runtimeKeys.ts +27 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,57 +1,70 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
# @chipmobilesdk/rn-i18n
|
|
2
|
+
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Packaging only; no code change. The package no longer declares `engines.node`: a
|
|
8
|
+
React Native package does not run on Node, so the field only produced install
|
|
9
|
+
warnings in apps on another Node version. rn-i18n's command-line tooling does run
|
|
10
|
+
on Node; its README's compatibility table states the version it needs. ([#14](https://github.com/operationd8979/chip-mobile-sdk/pull/14))
|
|
11
|
+
|
|
12
|
+
## 0.1.2 — 2026-11-01
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **A throwing locale listener no longer breaks a locale switch.** Listeners
|
|
17
|
+
were notified without isolation, so one that threw skipped every listener
|
|
18
|
+
after it (React subscribers included) and made `setLocale` reject after the
|
|
19
|
+
switch but before the choice was persisted: the app showed one locale and
|
|
20
|
+
stored another. Each listener is now isolated and reported with
|
|
21
|
+
`console.warn`, and a listener unsubscribed by an earlier one during the
|
|
22
|
+
same change is no longer called. No code change is required.
|
|
23
|
+
|
|
24
|
+
## 0.1.1 — 2026-08-01
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- The package now ships its agent skill (`skills/`), which consuming apps
|
|
29
|
+
copy in with `scripts/sync-sdk-skills.mjs`. No code change.
|
|
30
|
+
|
|
31
|
+
## 0.1.0 — 2026-06-13
|
|
32
|
+
|
|
33
|
+
Initial release of the reusable mobile i18n and locale library.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `createI18n(config)` with eager configuration validation: duplicate locales,
|
|
38
|
+
unknown/duplicate/cyclic fallback chains, missing fallback resources, and
|
|
39
|
+
missing storage adapter are rejected before any localized content is served.
|
|
40
|
+
- Translation lookup with namespaced nested keys, ordered fallback-chain
|
|
41
|
+
resolution, `{{name}}` interpolation, package-owned pluralization, and a
|
|
42
|
+
deterministic missing-key policy (one console warning per failed lookup path).
|
|
43
|
+
- Runtime locale switching without restart, persisted through an app-injected
|
|
44
|
+
`LocaleStorageAdapter`, with recovery from unsupported persisted values.
|
|
45
|
+
- Device-locale initialization via an injectable provider (default backed by
|
|
46
|
+
Hermes `Intl`), with exact-match and single-language-match selection.
|
|
47
|
+
- Locale-aware formatting: `formatNumber`, `formatDate`, `formatCurrency` (via
|
|
48
|
+
Hermes `Intl`), and package-owned `formatUnit`/`convertUnit` across length,
|
|
49
|
+
mass, volume, and temperature dimensions.
|
|
50
|
+
- Declared runtime-only translation entries that resolve through the same
|
|
51
|
+
pipeline and are protected during synchronization.
|
|
52
|
+
- React bindings: `I18nProvider`, `useTranslation`, `useLocale`.
|
|
53
|
+
- Node-only extraction/synchronization CLI (`@chipmobilesdk/rn-i18n/tooling`):
|
|
54
|
+
regex-pattern source scanning, locale JSON create/add/preserve/remove,
|
|
55
|
+
ambiguous-usage and conflict reporting, malformed-file isolation, typed key
|
|
56
|
+
contract generation, and deterministic output.
|
|
57
|
+
|
|
58
|
+
### Adoption notes
|
|
59
|
+
|
|
60
|
+
- App-owned translations live in the consuming app (commonly
|
|
61
|
+
`src/i18n/locales/<locale>.json`), never inside this package.
|
|
62
|
+
- Host frameworks (`react`, `react-native`) are peer dependencies; this package
|
|
63
|
+
has zero runtime dependencies.
|
|
64
|
+
- No native modules, permissions, network calls, analytics, or tracking are
|
|
65
|
+
introduced; the locale preference stays on-device via the injected adapter.
|
|
66
|
+
|
|
67
|
+
### Compatibility
|
|
68
|
+
|
|
69
|
+
React `19.x`, React Native `0.85+` (Hermes), TypeScript `5.8+`, Node `>=22.11.0`
|
|
70
|
+
for tooling. Android and iOS, with no native code.
|
package/README.md
CHANGED
|
@@ -1,218 +1,218 @@
|
|
|
1
|
-
# @chipmobilesdk/rn-i18n
|
|
2
|
-
|
|
3
|
-
Reusable localization for ChipMobileSdk-family React Native apps: per-locale
|
|
4
|
-
configuration, app-owned namespaced translations, typed keys, interpolation,
|
|
5
|
-
pluralization, fallback chains, runtime locale switching with app-injected
|
|
6
|
-
persistence, device-locale detection, locale-aware number/date/currency/unit
|
|
7
|
-
formatting, and a Node CLI that extracts and synchronizes translation keys.
|
|
8
|
-
|
|
9
|
-
> **License:** `UNLICENSED`. Published publicly for use by the owner's
|
|
10
|
-
> applications; no open-source license is granted.
|
|
11
|
-
|
|
12
|
-
- **Zero runtime dependencies.** Formatting uses the `Intl` APIs built into
|
|
13
|
-
Hermes; plural rules and unit conversion are package-owned.
|
|
14
|
-
- **No native code.** Storage and (optionally) device-locale detection are
|
|
15
|
-
injected by the consuming app.
|
|
16
|
-
- **Public package.** Published under the `@chipmobilesdk` npm organization.
|
|
17
|
-
- **Version**:
|
|
18
|
-
- **Entrypoints**: `@chipmobilesdk/rn-i18n` and the Node-only
|
|
19
|
-
`@chipmobilesdk/rn-i18n/tooling`
|
|
20
|
-
|
|
21
|
-
## Installation
|
|
22
|
-
|
|
23
|
-
This package ships TypeScript sources and is consumed through the workspace or
|
|
24
|
-
the public npm registry:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npm install @chipmobilesdk/rn-i18n
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Host frameworks are peer dependencies:
|
|
31
|
-
|
|
32
|
-
```jsonc
|
|
33
|
-
// peerDependencies
|
|
34
|
-
"react": ">=19",
|
|
35
|
-
"react-native": ">=0.85"
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Quick start
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
import { createI18n } from '@chipmobilesdk/rn-i18n';
|
|
42
|
-
import enUS from './locales/en-US.json';
|
|
43
|
-
import viVN from './locales/vi-VN.json';
|
|
44
|
-
import { storageAdapter } from './storage';
|
|
45
|
-
import type { AppI18nKey } from './keys.generated';
|
|
46
|
-
|
|
47
|
-
export const i18n = createI18n<AppI18nKey>({
|
|
48
|
-
primaryLocale: 'en-US',
|
|
49
|
-
locales: [
|
|
50
|
-
{
|
|
51
|
-
code: 'en-US',
|
|
52
|
-
displayName: 'English (US)',
|
|
53
|
-
direction: 'ltr',
|
|
54
|
-
fallbackChain: [],
|
|
55
|
-
defaultCurrency: 'USD',
|
|
56
|
-
defaultUnit: 'mile',
|
|
57
|
-
defaultDateFormat: { year: 'numeric', month: 'short', day: 'numeric' },
|
|
58
|
-
defaultNumberFormat: { maximumFractionDigits: 2 },
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
code: 'vi-VN',
|
|
62
|
-
displayName: 'Tiếng Việt',
|
|
63
|
-
direction: 'ltr',
|
|
64
|
-
fallbackChain: ['en-US'],
|
|
65
|
-
defaultCurrency: 'VND',
|
|
66
|
-
defaultUnit: 'kilometer',
|
|
67
|
-
defaultDateFormat: { day: '2-digit', month: '2-digit', year: 'numeric' },
|
|
68
|
-
defaultNumberFormat: { maximumFractionDigits: 2 },
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
resources: { 'en-US': enUS, 'vi-VN': viVN },
|
|
72
|
-
storage: storageAdapter,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await i18n.init(); // persisted -> device -> primary fallback
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Wrap the app and use the hooks:
|
|
79
|
-
|
|
80
|
-
```tsx
|
|
81
|
-
import { I18nProvider, useTranslation, useLocale } from '@chipmobilesdk/rn-i18n';
|
|
82
|
-
|
|
83
|
-
<I18nProvider i18n={i18n}>
|
|
84
|
-
<App />
|
|
85
|
-
</I18nProvider>;
|
|
86
|
-
|
|
87
|
-
const { t } = useTranslation<AppI18nKey>();
|
|
88
|
-
const { locale, direction, supportedLocales, setLocale } = useLocale();
|
|
89
|
-
|
|
90
|
-
t('welcome.message', { name: 'Dung' });
|
|
91
|
-
t('cart.items', { count }, { count }); // plural variant by count
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Public API
|
|
95
|
-
|
|
96
|
-
| Capability | Member |
|
|
97
|
-
|------------|--------|
|
|
98
|
-
| Setup + validation | `createI18n(config)`, `I18nConfigError` |
|
|
99
|
-
| Translation lookup | `i18n.t`, `useTranslation` |
|
|
100
|
-
| Locale state + switching | `i18n.getLocale`, `i18n.setLocale`, `useLocale` |
|
|
101
|
-
| Locale metadata (RTL/LTR, defaults) | `i18n.getSupportedLocales` |
|
|
102
|
-
| Device-locale init | `i18n.init`, `defaultDeviceLocaleProvider` |
|
|
103
|
-
| Formatting | `i18n.formatNumber/formatDate/formatCurrency/formatUnit` |
|
|
104
|
-
| Unit conversion | `i18n.convertUnit`, `convertUnit`, `getUnitSymbol`, `SUPPORTED_UNITS`, `UnitConversionError` |
|
|
105
|
-
| Runtime-only declarations | `i18n.registerRuntimeEntries` |
|
|
106
|
-
| Persistence key | `LOCALE_STORAGE_KEY` |
|
|
107
|
-
| React | `I18nProvider` |
|
|
108
|
-
|
|
109
|
-
Public contract types also include the configuration, locale, translation,
|
|
110
|
-
formatting, unit, provider/adapter, and hook result types exported by
|
|
111
|
-
`src/index.ts`. Do not deep-import package internals. Node-only extraction and
|
|
112
|
-
synchronization APIs must be imported from `@chipmobilesdk/rn-i18n/tooling`.
|
|
113
|
-
|
|
114
|
-
### Storage adapter (required for persistence)
|
|
115
|
-
|
|
116
|
-
The package never bundles a storage backend (FR-046/FR-047). Inject one:
|
|
117
|
-
|
|
118
|
-
```ts
|
|
119
|
-
import type { LocaleStorageAdapter } from '@chipmobilesdk/rn-i18n';
|
|
120
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
121
|
-
|
|
122
|
-
export const storageAdapter: LocaleStorageAdapter = {
|
|
123
|
-
getItem: key => AsyncStorage.getItem(key),
|
|
124
|
-
setItem: (key, value) => AsyncStorage.setItem(key, value),
|
|
125
|
-
removeItem: key => AsyncStorage.removeItem(key),
|
|
126
|
-
};
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
An MMKV-backed adapter (wrapping its synchronous API in `Promise.resolve`)
|
|
130
|
-
works equally well. If persistence is enabled without an adapter, `createI18n`
|
|
131
|
-
throws before any localized content is served.
|
|
132
|
-
|
|
133
|
-
### Device-locale provider (optional)
|
|
134
|
-
|
|
135
|
-
The default reads `Intl.DateTimeFormat().resolvedOptions().locale`. Apps that
|
|
136
|
-
need richer detection can inject `deviceLocale` (e.g. backed by
|
|
137
|
-
`react-native-localize`); that dependency stays in the app, never the package.
|
|
138
|
-
|
|
139
|
-
## Translations
|
|
140
|
-
|
|
141
|
-
App-owned JSON, organized by locale and namespace; nested dot-path keys,
|
|
142
|
-
`{{name}}` interpolation, and plural groups:
|
|
143
|
-
|
|
144
|
-
```jsonc
|
|
145
|
-
{
|
|
146
|
-
"welcome": { "message": "Welcome, {{name}}!" },
|
|
147
|
-
"cart": { "items": { "one": "{{count}} item", "other": "{{count}} items" } }
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Plural categories deliberately come from package-owned rules rather than
|
|
152
|
-
depending on `Intl.PluralRules`, which keeps behavior consistent across the
|
|
153
|
-
supported Hermes environments. Built-ins cover `en` (one/other), `vi`/`ja`
|
|
154
|
-
(other-only), and a default `count === 1 -> one` rule; supply
|
|
155
|
-
`LocaleConfig.pluralRule` for richer languages.
|
|
156
|
-
|
|
157
|
-
## Extraction & synchronization CLI
|
|
158
|
-
|
|
159
|
-
Node-only, exposed via `@chipmobilesdk/rn-i18n/tooling` and a CLI. Add an app
|
|
160
|
-
config (`i18n-sync.config.json`) and a script:
|
|
161
|
-
|
|
162
|
-
```jsonc
|
|
163
|
-
{
|
|
164
|
-
"sourceGlobs": ["src/**/*.ts", "src/**/*.tsx"],
|
|
165
|
-
"patterns": [{ "regex": "[^\\w.]t\\(\\s*'(?<key>[A-Za-z0-9_.-]+)'" }],
|
|
166
|
-
"localesDir": "src/i18n/locales",
|
|
167
|
-
"locales": ["en-US", "vi-VN", "ja-JP"],
|
|
168
|
-
"runtimeEntriesFile": "src/i18n/runtime-keys.ts",
|
|
169
|
-
"generateKeysFile": "src/i18n/keys.generated.ts",
|
|
170
|
-
"keysTypeName": "AppI18nKey"
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
```jsonc
|
|
175
|
-
// package.json
|
|
176
|
-
"scripts": {
|
|
177
|
-
"i18n:sync": "node --experimental-strip-types node_modules/@chipmobilesdk/rn-i18n/src/tooling/cli.ts --config i18n-sync.config.json"
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Running it scans the source tree, then per locale: creates missing files, adds
|
|
182
|
-
missing keys (with the placeholder policy), preserves existing translations,
|
|
183
|
-
removes stale keys, protects declared runtime-only keys, regenerates the typed
|
|
184
|
-
key contract, and prints a report. Exit codes: `0` success/warnings, `1`
|
|
185
|
-
blocking findings (conflicts, malformed locale files), `2` invalid config.
|
|
186
|
-
|
|
187
|
-
Bind `t()` to the generated `AppI18nKey` so `tsc --noEmit` reports invalid or
|
|
188
|
-
stale key usage before release.
|
|
189
|
-
|
|
190
|
-
### Runtime-only keys
|
|
191
|
-
|
|
192
|
-
Keys resolved only at runtime (declared in `runtimeEntriesFile`) use the same
|
|
193
|
-
lookup, fallback, interpolation, and missing-key behavior, and are never
|
|
194
|
-
removed by synchronization. Policy `add` writes the default text to missing
|
|
195
|
-
locales; `report` only lists them.
|
|
196
|
-
|
|
197
|
-
## Compatibility
|
|
198
|
-
|
|
199
|
-
| Surface | Baseline |
|
|
200
|
-
|---------|----------|
|
|
201
|
-
| React / React Native | `19.x` / `0.85+` (Hermes) |
|
|
202
|
-
| TypeScript | `5.8+` |
|
|
203
|
-
| Node (tooling) | `>=22.11.0` |
|
|
204
|
-
| Platforms | Android, iOS (no native code) |
|
|
205
|
-
|
|
206
|
-
## Validation
|
|
207
|
-
|
|
208
|
-
```bash
|
|
209
|
-
npm test # package + demo suites
|
|
210
|
-
npm run typecheck
|
|
211
|
-
npm run pack:i18n # npm pack --workspace @chipmobilesdk/rn-i18n --dry-run
|
|
212
|
-
npm run i18n:sync # extraction + synchronization
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
The demo integration lives in `src/screens/I18nDemoScreen.tsx` and uses
|
|
216
|
-
`src/i18n/` for setup, translations, persistence, and generated keys.
|
|
217
|
-
|
|
218
|
-
See [CHANGELOG.md](CHANGELOG.md) for release notes and adoption guidance.
|
|
1
|
+
# @chipmobilesdk/rn-i18n
|
|
2
|
+
|
|
3
|
+
Reusable localization for ChipMobileSdk-family React Native apps: per-locale
|
|
4
|
+
configuration, app-owned namespaced translations, typed keys, interpolation,
|
|
5
|
+
pluralization, fallback chains, runtime locale switching with app-injected
|
|
6
|
+
persistence, device-locale detection, locale-aware number/date/currency/unit
|
|
7
|
+
formatting, and a Node CLI that extracts and synchronizes translation keys.
|
|
8
|
+
|
|
9
|
+
> **License:** `UNLICENSED`. Published publicly for use by the owner's
|
|
10
|
+
> applications; no open-source license is granted.
|
|
11
|
+
|
|
12
|
+
- **Zero runtime dependencies.** Formatting uses the `Intl` APIs built into
|
|
13
|
+
Hermes; plural rules and unit conversion are package-owned.
|
|
14
|
+
- **No native code.** Storage and (optionally) device-locale detection are
|
|
15
|
+
injected by the consuming app.
|
|
16
|
+
- **Public package.** Published under the `@chipmobilesdk` npm organization.
|
|
17
|
+
- **Version**: see [CHANGELOG](CHANGELOG.md)
|
|
18
|
+
- **Entrypoints**: `@chipmobilesdk/rn-i18n` and the Node-only
|
|
19
|
+
`@chipmobilesdk/rn-i18n/tooling`
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
This package ships TypeScript sources and is consumed through the workspace or
|
|
24
|
+
the public npm registry:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @chipmobilesdk/rn-i18n
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Host frameworks are peer dependencies:
|
|
31
|
+
|
|
32
|
+
```jsonc
|
|
33
|
+
// peerDependencies
|
|
34
|
+
"react": ">=19",
|
|
35
|
+
"react-native": ">=0.85"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { createI18n } from '@chipmobilesdk/rn-i18n';
|
|
42
|
+
import enUS from './locales/en-US.json';
|
|
43
|
+
import viVN from './locales/vi-VN.json';
|
|
44
|
+
import { storageAdapter } from './storage';
|
|
45
|
+
import type { AppI18nKey } from './keys.generated';
|
|
46
|
+
|
|
47
|
+
export const i18n = createI18n<AppI18nKey>({
|
|
48
|
+
primaryLocale: 'en-US',
|
|
49
|
+
locales: [
|
|
50
|
+
{
|
|
51
|
+
code: 'en-US',
|
|
52
|
+
displayName: 'English (US)',
|
|
53
|
+
direction: 'ltr',
|
|
54
|
+
fallbackChain: [],
|
|
55
|
+
defaultCurrency: 'USD',
|
|
56
|
+
defaultUnit: 'mile',
|
|
57
|
+
defaultDateFormat: { year: 'numeric', month: 'short', day: 'numeric' },
|
|
58
|
+
defaultNumberFormat: { maximumFractionDigits: 2 },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
code: 'vi-VN',
|
|
62
|
+
displayName: 'Tiếng Việt',
|
|
63
|
+
direction: 'ltr',
|
|
64
|
+
fallbackChain: ['en-US'],
|
|
65
|
+
defaultCurrency: 'VND',
|
|
66
|
+
defaultUnit: 'kilometer',
|
|
67
|
+
defaultDateFormat: { day: '2-digit', month: '2-digit', year: 'numeric' },
|
|
68
|
+
defaultNumberFormat: { maximumFractionDigits: 2 },
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
resources: { 'en-US': enUS, 'vi-VN': viVN },
|
|
72
|
+
storage: storageAdapter,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await i18n.init(); // persisted -> device -> primary fallback
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Wrap the app and use the hooks:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { I18nProvider, useTranslation, useLocale } from '@chipmobilesdk/rn-i18n';
|
|
82
|
+
|
|
83
|
+
<I18nProvider i18n={i18n}>
|
|
84
|
+
<App />
|
|
85
|
+
</I18nProvider>;
|
|
86
|
+
|
|
87
|
+
const { t } = useTranslation<AppI18nKey>();
|
|
88
|
+
const { locale, direction, supportedLocales, setLocale } = useLocale();
|
|
89
|
+
|
|
90
|
+
t('welcome.message', { name: 'Dung' });
|
|
91
|
+
t('cart.items', { count }, { count }); // plural variant by count
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Public API
|
|
95
|
+
|
|
96
|
+
| Capability | Member |
|
|
97
|
+
|------------|--------|
|
|
98
|
+
| Setup + validation | `createI18n(config)`, `I18nConfigError` |
|
|
99
|
+
| Translation lookup | `i18n.t`, `useTranslation` |
|
|
100
|
+
| Locale state + switching | `i18n.getLocale`, `i18n.setLocale`, `useLocale` |
|
|
101
|
+
| Locale metadata (RTL/LTR, defaults) | `i18n.getSupportedLocales` |
|
|
102
|
+
| Device-locale init | `i18n.init`, `defaultDeviceLocaleProvider` |
|
|
103
|
+
| Formatting | `i18n.formatNumber/formatDate/formatCurrency/formatUnit` |
|
|
104
|
+
| Unit conversion | `i18n.convertUnit`, `convertUnit`, `getUnitSymbol`, `SUPPORTED_UNITS`, `UnitConversionError` |
|
|
105
|
+
| Runtime-only declarations | `i18n.registerRuntimeEntries` |
|
|
106
|
+
| Persistence key | `LOCALE_STORAGE_KEY` |
|
|
107
|
+
| React | `I18nProvider` |
|
|
108
|
+
|
|
109
|
+
Public contract types also include the configuration, locale, translation,
|
|
110
|
+
formatting, unit, provider/adapter, and hook result types exported by
|
|
111
|
+
`src/index.ts`. Do not deep-import package internals. Node-only extraction and
|
|
112
|
+
synchronization APIs must be imported from `@chipmobilesdk/rn-i18n/tooling`.
|
|
113
|
+
|
|
114
|
+
### Storage adapter (required for persistence)
|
|
115
|
+
|
|
116
|
+
The package never bundles a storage backend (FR-046/FR-047). Inject one:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import type { LocaleStorageAdapter } from '@chipmobilesdk/rn-i18n';
|
|
120
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
121
|
+
|
|
122
|
+
export const storageAdapter: LocaleStorageAdapter = {
|
|
123
|
+
getItem: key => AsyncStorage.getItem(key),
|
|
124
|
+
setItem: (key, value) => AsyncStorage.setItem(key, value),
|
|
125
|
+
removeItem: key => AsyncStorage.removeItem(key),
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
An MMKV-backed adapter (wrapping its synchronous API in `Promise.resolve`)
|
|
130
|
+
works equally well. If persistence is enabled without an adapter, `createI18n`
|
|
131
|
+
throws before any localized content is served.
|
|
132
|
+
|
|
133
|
+
### Device-locale provider (optional)
|
|
134
|
+
|
|
135
|
+
The default reads `Intl.DateTimeFormat().resolvedOptions().locale`. Apps that
|
|
136
|
+
need richer detection can inject `deviceLocale` (e.g. backed by
|
|
137
|
+
`react-native-localize`); that dependency stays in the app, never the package.
|
|
138
|
+
|
|
139
|
+
## Translations
|
|
140
|
+
|
|
141
|
+
App-owned JSON, organized by locale and namespace; nested dot-path keys,
|
|
142
|
+
`{{name}}` interpolation, and plural groups:
|
|
143
|
+
|
|
144
|
+
```jsonc
|
|
145
|
+
{
|
|
146
|
+
"welcome": { "message": "Welcome, {{name}}!" },
|
|
147
|
+
"cart": { "items": { "one": "{{count}} item", "other": "{{count}} items" } }
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Plural categories deliberately come from package-owned rules rather than
|
|
152
|
+
depending on `Intl.PluralRules`, which keeps behavior consistent across the
|
|
153
|
+
supported Hermes environments. Built-ins cover `en` (one/other), `vi`/`ja`
|
|
154
|
+
(other-only), and a default `count === 1 -> one` rule; supply
|
|
155
|
+
`LocaleConfig.pluralRule` for richer languages.
|
|
156
|
+
|
|
157
|
+
## Extraction & synchronization CLI
|
|
158
|
+
|
|
159
|
+
Node-only, exposed via `@chipmobilesdk/rn-i18n/tooling` and a CLI. Add an app
|
|
160
|
+
config (`i18n-sync.config.json`) and a script:
|
|
161
|
+
|
|
162
|
+
```jsonc
|
|
163
|
+
{
|
|
164
|
+
"sourceGlobs": ["src/**/*.ts", "src/**/*.tsx"],
|
|
165
|
+
"patterns": [{ "regex": "[^\\w.]t\\(\\s*'(?<key>[A-Za-z0-9_.-]+)'" }],
|
|
166
|
+
"localesDir": "src/i18n/locales",
|
|
167
|
+
"locales": ["en-US", "vi-VN", "ja-JP"],
|
|
168
|
+
"runtimeEntriesFile": "src/i18n/runtime-keys.ts",
|
|
169
|
+
"generateKeysFile": "src/i18n/keys.generated.ts",
|
|
170
|
+
"keysTypeName": "AppI18nKey"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```jsonc
|
|
175
|
+
// package.json
|
|
176
|
+
"scripts": {
|
|
177
|
+
"i18n:sync": "node --experimental-strip-types node_modules/@chipmobilesdk/rn-i18n/src/tooling/cli.ts --config i18n-sync.config.json"
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Running it scans the source tree, then per locale: creates missing files, adds
|
|
182
|
+
missing keys (with the placeholder policy), preserves existing translations,
|
|
183
|
+
removes stale keys, protects declared runtime-only keys, regenerates the typed
|
|
184
|
+
key contract, and prints a report. Exit codes: `0` success/warnings, `1`
|
|
185
|
+
blocking findings (conflicts, malformed locale files), `2` invalid config.
|
|
186
|
+
|
|
187
|
+
Bind `t()` to the generated `AppI18nKey` so `tsc --noEmit` reports invalid or
|
|
188
|
+
stale key usage before release.
|
|
189
|
+
|
|
190
|
+
### Runtime-only keys
|
|
191
|
+
|
|
192
|
+
Keys resolved only at runtime (declared in `runtimeEntriesFile`) use the same
|
|
193
|
+
lookup, fallback, interpolation, and missing-key behavior, and are never
|
|
194
|
+
removed by synchronization. Policy `add` writes the default text to missing
|
|
195
|
+
locales; `report` only lists them.
|
|
196
|
+
|
|
197
|
+
## Compatibility
|
|
198
|
+
|
|
199
|
+
| Surface | Baseline |
|
|
200
|
+
|---------|----------|
|
|
201
|
+
| React / React Native | `19.x` / `0.85+` (Hermes) |
|
|
202
|
+
| TypeScript | `5.8+` |
|
|
203
|
+
| Node (tooling) | `>=22.11.0` |
|
|
204
|
+
| Platforms | Android, iOS (no native code) |
|
|
205
|
+
|
|
206
|
+
## Validation
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
npm test # package + demo suites
|
|
210
|
+
npm run typecheck --workspace @chipmobilesdk/rn-i18n # tsc -p packages/rn-i18n/tsconfig.json --noEmit
|
|
211
|
+
npm run pack:check --workspace @chipmobilesdk/rn-i18n # npm pack --workspace @chipmobilesdk/rn-i18n --dry-run
|
|
212
|
+
npm run i18n:sync # extraction + synchronization
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The demo integration lives in `src/screens/I18nDemoScreen.tsx` and uses
|
|
216
|
+
`src/i18n/` for setup, translations, persistence, and generated keys.
|
|
217
|
+
|
|
218
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes and adoption guidance.
|