@chipmobilesdk/rn-i18n 0.1.2 → 0.2.0

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.
@@ -1,62 +1,65 @@
1
- ---
2
- name: sdk-rn-i18n
3
- description: "Localization via @chipmobilesdk/rn-i18n: createI18n, useTranslation, useLocale, locale persistence, unit formatting, and the extraction/sync CLI. Use when adding translated strings, switching locale, formatting units, or syncing translation keys."
4
- metadata:
5
- package: "@chipmobilesdk/rn-i18n"
6
- packageVersion: "0.1.0"
7
- user-invocable: true
8
- disable-model-invocation: false
9
- ---
10
-
11
- # @chipmobilesdk/rn-i18n
12
-
13
- Localization with device-locale detection, persisted locale choice, typed translation
14
- access, unit/number formatting, and a CLI that extracts and synchronizes keys.
15
-
16
- ## Use this when
17
-
18
- - Adding or changing any user-facing string
19
- - Reading or switching the active locale
20
- - Formatting numbers, dates or units for a locale
21
- - Translation keys are missing, stale, or out of sync across locale files
22
-
23
- ## Never hardcode a user-facing string
24
-
25
- Every string rendered to a user goes through `useTranslation`. A literal in a component
26
- is a defect — it cannot be translated and will not appear in extraction output.
27
-
28
- ## Keys are generated, not hand-maintained
29
-
30
- Do not hand-edit locale JSON to add a key. Run the extraction/sync CLI so every locale
31
- file stays aligned. The monorepo exposes it as `npm run i18n:sync`. See
32
- `## Extraction & synchronization CLI`.
33
-
34
- ## Public API (v0.1.2)
35
-
36
- Runtime:
37
-
38
- - `createI18n(...)` → `I18n`
39
- - `I18nProvider` (+ `I18nProviderProps`)
40
- - `useTranslation()` → `UseTranslationResult`
41
- - `useLocale()` → `UseLocaleResult`
42
- - `defaultDeviceLocaleProvider` — device locale detection
43
- - `LOCALE_STORAGE_KEY` — where the chosen locale persists
44
- - `I18nConfigError` — thrown on invalid configuration at startup
45
-
46
- Types: `I18n`, `UnitFormatOptions`, `I18nProviderProps`, `UseLocaleResult`,
47
- `UseTranslationResult`
48
-
49
- `I18nConfigError` throws at startup by design — a misconfigured i18n setup should fail
50
- loudly, not silently render keys.
51
-
52
- ## Where the detail lives
53
-
54
- Read one section of `node_modules/@chipmobilesdk/rn-i18n/README.md`:
55
-
56
- | Need | Section |
57
- |---|---|
58
- | First integration | `## Installation`, `## Quick start` |
59
- | Full export list | `## Public API` |
60
- | Locale file layout, plurals | `## Translations` |
61
- | Adding/syncing keys | `## Extraction & synchronization CLI` |
62
- | Version/peer constraints | `## Compatibility` |
1
+ ---
2
+ name: sdk-rn-i18n
3
+ description: "Localization via @chipmobilesdk/rn-i18n: createI18n, useTranslation, useLocale, locale persistence, unit formatting, and the extraction/sync CLI. Use when adding translated strings, switching locale, formatting units, or syncing translation keys."
4
+ metadata:
5
+ package: "@chipmobilesdk/rn-i18n"
6
+ user-invocable: true
7
+ disable-model-invocation: false
8
+ ---
9
+
10
+ # @chipmobilesdk/rn-i18n
11
+
12
+ Localization with device-locale detection, persisted locale choice, typed translation
13
+ access, unit/number formatting, and a CLI that extracts and synchronizes keys.
14
+
15
+ ## Use this when
16
+
17
+ - Adding or changing any user-facing string
18
+ - Reading or switching the active locale
19
+ - Formatting numbers, dates or units for a locale
20
+ - Translation keys are missing, stale, or out of sync across locale files
21
+
22
+ ## Never hardcode a user-facing string
23
+
24
+ Every string rendered to a user goes through `useTranslation`. A literal in a component
25
+ is a defect — it cannot be translated and will not appear in extraction output.
26
+
27
+ ## Keys are generated, not hand-maintained
28
+
29
+ Do not hand-edit locale JSON to add a key. Run the extraction/sync CLI so every locale
30
+ file stays aligned. The monorepo exposes it as `npm run i18n:sync`. See
31
+ `## Extraction & synchronization CLI`.
32
+
33
+ ## Public API
34
+
35
+ Runtime:
36
+
37
+ - `createI18n(...)` → `I18n`
38
+ - `I18nProvider` (+ `I18nProviderProps`)
39
+ - `useTranslation()` → `UseTranslationResult`
40
+ - `useLocale()` → `UseLocaleResult`
41
+ - `defaultDeviceLocaleProvider` — device locale detection
42
+ - `LOCALE_STORAGE_KEY` — where the chosen locale persists
43
+ - `i18n.initSync()` — resolve the locale before the first render when storage reads
44
+ synchronously; `localeStorageFromKeyValue(port)` makes such storage from an rn-core
45
+ key-value port (MMKV)
46
+ - `I18nConfigError` — thrown on invalid configuration at startup; an rn-core SDK error with an
47
+ `I18nErrorCode`
48
+
49
+ Types: `I18n`, `UnitFormatOptions`, `I18nProviderProps`, `UseLocaleResult`,
50
+ `UseTranslationResult`
51
+
52
+ `I18nConfigError` throws at startup by design — a misconfigured i18n setup should fail
53
+ loudly, not silently render keys.
54
+
55
+ ## Where the detail lives
56
+
57
+ Read one section of `node_modules/@chipmobilesdk/rn-i18n/README.md`:
58
+
59
+ | Need | Section |
60
+ |---|---|
61
+ | First integration | `## Installation`, `## Quick start` |
62
+ | Full export list | `## Public API` |
63
+ | Locale file layout, plurals | `## Translations` |
64
+ | Adding/syncing keys | `## Extraction & synchronization CLI` |
65
+ | Version/peer constraints | `## Compatibility` |
@@ -0,0 +1,25 @@
1
+ import type { RuntimeTranslationEntry } from './types';
2
+
3
+ /**
4
+ * Runtime entries for a package's message catalog (I18N-13), so an app registers
5
+ * an SDK's copy in one line and translates it like its own:
6
+ *
7
+ * ```ts
8
+ * createI18n({ ...config, runtimeEntries: runtimeEntriesFromCatalog(paymentMessages.user) });
9
+ * ```
10
+ *
11
+ * Catalogs mark variables `{name}`; rn-i18n's are `{{name}}`, so they are
12
+ * converted. `policy: 'add'` lets the sync CLI write missing keys into each locale
13
+ * file with the default text.
14
+ */
15
+ export function runtimeEntriesFromCatalog(
16
+ catalog: Readonly<Record<string, string>>,
17
+ options: { readonly policy?: 'add' | 'report'; readonly description?: string } = {},
18
+ ): RuntimeTranslationEntry[] {
19
+ return Object.entries(catalog).map(([key, text]) => ({
20
+ key,
21
+ defaultText: text.replace(/(?<!\{)\{(\w+)\}(?!\})/g, '{{$1}}'),
22
+ policy: options.policy ?? 'report',
23
+ ...(options.description === undefined ? {} : { description: options.description }),
24
+ }));
25
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ createDiagnosticChannel,
3
+ redactPatterns,
4
+ type DiagnosticSeverity,
5
+ } from '@chipmobilesdk/rn-core';
6
+
7
+ /** What rn-i18n reports. Stable: an app may branch on these. */
8
+ export type I18nDiagnosticCode =
9
+ | 'missing_translation'
10
+ | 'fallback_used'
11
+ | 'missing_variable'
12
+ | 'invalid_value'
13
+ | 'invalid_format'
14
+ | 'locale_unsupported'
15
+ | 'locale_read_failed'
16
+ | 'locale_write_failed'
17
+ | 'listener_threw';
18
+
19
+ export interface I18nDiagnostic {
20
+ readonly code: I18nDiagnosticCode;
21
+ /** For people: names the key, locale or variable involved. */
22
+ readonly message: string;
23
+ readonly key?: string;
24
+ readonly locale?: string;
25
+ }
26
+
27
+ export type I18nDiagnosticHandler = (diagnostic: I18nDiagnostic) => void;
28
+
29
+ const SEVERITY: Record<I18nDiagnosticCode, DiagnosticSeverity> = {
30
+ missing_translation: 'warn',
31
+ fallback_used: 'info',
32
+ missing_variable: 'warn',
33
+ invalid_value: 'warn',
34
+ invalid_format: 'warn',
35
+ locale_unsupported: 'warn',
36
+ locale_read_failed: 'warn',
37
+ locale_write_failed: 'warn',
38
+ listener_threw: 'error',
39
+ };
40
+
41
+ const channel = createDiagnosticChannel<I18nDiagnosticCode>('rn-i18n');
42
+
43
+ /**
44
+ * One place every diagnostic goes through (I18N-11): the app's SDK sinks always
45
+ * (`installSdkSink` from rn-core), then `onDiagnostic` when the app gave one,
46
+ * otherwise `console.warn` in development builds and tests only.
47
+ */
48
+ export function createReporter(onDiagnostic?: I18nDiagnosticHandler): I18nDiagnosticHandler {
49
+ return diagnostic => {
50
+ channel.emit(diagnostic.code, SEVERITY[diagnostic.code], {
51
+ ...(diagnostic.key === undefined ? {} : { key: diagnostic.key }),
52
+ ...(diagnostic.locale === undefined ? {} : { locale: diagnostic.locale }),
53
+ });
54
+ if (onDiagnostic) {
55
+ onDiagnostic(diagnostic);
56
+ } else if (typeof __DEV__ === 'undefined' || __DEV__) {
57
+ console.warn(`[rn-i18n] ${diagnostic.message}`);
58
+ }
59
+ };
60
+ }
61
+
62
+ /** For the few call sites that run before or outside an i18n instance. */
63
+ export const defaultReporter = createReporter();
64
+
65
+ /** Foreign error text, shaped before it reaches a message. */
66
+ export function describeError(error: unknown): string {
67
+ return redactPatterns(error instanceof Error ? error.message : String(error));
68
+ }
@@ -0,0 +1,28 @@
1
+ import { SdkError } from '@chipmobilesdk/rn-core';
2
+
3
+ /** The package's error codes. */
4
+ export type I18nErrorCode = 'invalid_config' | 'unit_conversion';
5
+
6
+ /**
7
+ * Configuration error listing every detected violation. An `SdkError`
8
+ * (`@chipmobilesdk/rn-core`) with `pkg: 'rn-i18n'`.
9
+ */
10
+ export class I18nConfigError extends SdkError<I18nErrorCode> {
11
+ readonly issues: readonly string[];
12
+
13
+ constructor(issues: readonly string[]) {
14
+ super('rn-i18n', 'invalid_config', {
15
+ message: `Invalid i18n configuration:\n- ${issues.join('\n- ')}`,
16
+ });
17
+ this.name = 'I18nConfigError';
18
+ this.issues = issues;
19
+ }
20
+ }
21
+
22
+ /** A unit conversion the package cannot perform, such as across dimensions. */
23
+ export class UnitConversionError extends SdkError<I18nErrorCode> {
24
+ constructor(message: string) {
25
+ super('rn-i18n', 'unit_conversion', { message });
26
+ this.name = 'UnitConversionError';
27
+ }
28
+ }
@@ -1,152 +1,150 @@
1
- /**
2
- * Public configuration and contract types for @chipmobilesdk/rn-i18n.
3
- *
4
- * The package has zero runtime dependencies: formatting uses the Intl APIs
5
- * built into Hermes (Intl.NumberFormat, Intl.DateTimeFormat), while plural
6
- * rules and unit conversion are package-owned because Intl.PluralRules and
7
- * Intl unit-style formatting are not reliably available in Hermes.
8
- */
9
-
10
- export type LocaleDirection = 'ltr' | 'rtl';
11
-
12
- export type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
13
-
14
- export type PluralRule = (count: number) => PluralCategory;
15
-
16
- export type UnitDimension = 'length' | 'mass' | 'volume' | 'temperature';
17
-
18
- export type UnitId =
19
- | 'millimeter'
20
- | 'centimeter'
21
- | 'meter'
22
- | 'kilometer'
23
- | 'inch'
24
- | 'foot'
25
- | 'mile'
26
- | 'gram'
27
- | 'kilogram'
28
- | 'ounce'
29
- | 'pound'
30
- | 'milliliter'
31
- | 'liter'
32
- | 'gallon'
33
- | 'celsius'
34
- | 'fahrenheit'
35
- | 'kelvin';
36
-
37
- export interface LocaleConfig {
38
- /** BCP-47 language-region code, e.g. `vi-VN`, `en-US`, `ja-JP`. */
39
- code: string;
40
- displayName: string;
41
- direction: LocaleDirection;
42
- /**
43
- * Ordered locale codes consulted when a key is missing from this locale.
44
- * Every entry must be another supported locale; cycles, duplicates, and
45
- * self-references are rejected during setup validation.
46
- */
47
- fallbackChain: string[];
48
- /** ISO-4217 currency code used when no explicit currency is requested. */
49
- defaultCurrency: string;
50
- defaultUnit: UnitId;
51
- /**
52
- * Default Intl.DateTimeFormat options for this locale. Restricted to the
53
- * component options supported by Hermes on both Android and iOS.
54
- */
55
- defaultDateFormat: Intl.DateTimeFormatOptions;
56
- /** Default Intl.NumberFormat options for this locale. */
57
- defaultNumberFormat: Intl.NumberFormatOptions;
58
- /** Optional plural rule override; defaults to the built-in language rule. */
59
- pluralRule?: PluralRule;
60
- }
61
-
62
- export type TranslationNode = string | { [key: string]: TranslationNode };
63
-
64
- export type TranslationNamespace = { [key: string]: TranslationNode };
65
-
66
- /** App-owned translation resources keyed by locale code. */
67
- export type TranslationResources = { [localeCode: string]: TranslationNamespace };
68
-
69
- /**
70
- * A translation key intentionally resolved at runtime and therefore not
71
- * discoverable by static source scanning. Declared entries are protected
72
- * from removal during locale JSON synchronization.
73
- */
74
- export interface RuntimeTranslationEntry {
75
- key: string;
76
- /** Template text used when no locale resource provides a value. */
77
- defaultText: string;
78
- /**
79
- * Synchronization policy when the key is missing from a locale file:
80
- * `add` writes the default text, `report` only lists it. Default: `report`.
81
- */
82
- policy?: 'add' | 'report';
83
- /** Owning feature note surfaced in the extraction report. */
84
- description?: string;
85
- }
86
-
87
- export type MissingTranslationPolicy =
88
- | { mode: 'key' }
89
- | { mode: 'placeholder'; text: string };
90
-
91
- export type InvalidFormatPolicy =
92
- | { mode: 'placeholder'; text: string }
93
- | { mode: 'error' };
94
-
95
- /** App-injected persistence contract. The package never bundles a backend. */
96
- export interface LocaleStorageAdapter {
97
- getItem(key: string): Promise<string | null>;
98
- setItem(key: string, value: string): Promise<void>;
99
- removeItem(key: string): Promise<void>;
100
- }
101
-
102
- /** Returns the device locale, or undefined when detection is unavailable. */
103
- export type DeviceLocaleProvider = () => string | undefined;
104
-
105
- export interface I18nConfig {
106
- locales: LocaleConfig[];
107
- /** Recovery locale when persistence or device detection cannot decide. */
108
- primaryLocale: string;
109
- resources: TranslationResources;
110
- runtimeEntries?: RuntimeTranslationEntry[];
111
- /** Persist the selected locale across launches. Default: true. */
112
- persistLocale?: boolean;
113
- /** Required when `persistLocale` is not disabled. */
114
- storage?: LocaleStorageAdapter;
115
- deviceLocale?: DeviceLocaleProvider;
116
- /** Default: `{ mode: 'key' }`. */
117
- missingTranslation?: MissingTranslationPolicy;
118
- /** Default: `{ mode: 'placeholder', text: '—' }`. */
119
- invalidFormat?: InvalidFormatPolicy;
120
- }
121
-
122
- export type LocaleSelectionSource =
123
- | 'persisted'
124
- | 'device'
125
- | 'languageMatch'
126
- | 'primaryFallback'
127
- | 'runtimeSwitch';
128
-
129
- export interface ActiveLocalePreference {
130
- code: string;
131
- source: LocaleSelectionSource;
132
- /** True when an unsupported persisted locale was discarded. */
133
- recovered: boolean;
134
- }
135
-
136
- export type TranslateVars = Record<string, string | number>;
137
-
138
- export interface TranslateOptions {
139
- /** Selects the plural variant and is exposed as the `count` variable. */
140
- count?: number;
141
- }
142
-
143
- /** Configuration error listing every detected violation. */
144
- export class I18nConfigError extends Error {
145
- readonly issues: readonly string[];
146
-
147
- constructor(issues: readonly string[]) {
148
- super(`Invalid i18n configuration:\n- ${issues.join('\n- ')}`);
149
- this.name = 'I18nConfigError';
150
- this.issues = issues;
151
- }
152
- }
1
+ /**
2
+ * Public configuration and contract types for @chipmobilesdk/rn-i18n.
3
+ *
4
+ * The package has one runtime dependency, rn-core; formatting uses the Intl APIs
5
+ * built into Hermes (Intl.NumberFormat, Intl.DateTimeFormat), while plural
6
+ * rules and unit conversion are package-owned because Intl.PluralRules and
7
+ * Intl unit-style formatting are not reliably available in Hermes.
8
+ */
9
+
10
+ import type { I18nDiagnosticHandler } from './diagnostics';
11
+
12
+ export type LocaleDirection = 'ltr' | 'rtl';
13
+
14
+ export type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
15
+
16
+ export type PluralRule = (count: number) => PluralCategory;
17
+
18
+ export type UnitDimension = 'length' | 'mass' | 'volume' | 'temperature';
19
+
20
+ export type UnitId =
21
+ | 'millimeter'
22
+ | 'centimeter'
23
+ | 'meter'
24
+ | 'kilometer'
25
+ | 'inch'
26
+ | 'foot'
27
+ | 'mile'
28
+ | 'gram'
29
+ | 'kilogram'
30
+ | 'ounce'
31
+ | 'pound'
32
+ | 'milliliter'
33
+ | 'liter'
34
+ | 'gallon'
35
+ | 'celsius'
36
+ | 'fahrenheit'
37
+ | 'kelvin';
38
+
39
+ export interface LocaleConfig {
40
+ /** BCP-47 language-region code, e.g. `vi-VN`, `en-US`, `ja-JP`. */
41
+ code: string;
42
+ displayName: string;
43
+ direction: LocaleDirection;
44
+ /**
45
+ * Ordered locale codes consulted when a key is missing from this locale.
46
+ * Every entry must be another supported locale; cycles, duplicates, and
47
+ * self-references are rejected during setup validation.
48
+ */
49
+ fallbackChain: string[];
50
+ /** ISO-4217 currency code used when no explicit currency is requested. */
51
+ defaultCurrency: string;
52
+ defaultUnit: UnitId;
53
+ /**
54
+ * Default Intl.DateTimeFormat options for this locale. Restricted to the
55
+ * component options supported by Hermes on both Android and iOS.
56
+ */
57
+ defaultDateFormat: Intl.DateTimeFormatOptions;
58
+ /** Default Intl.NumberFormat options for this locale. */
59
+ defaultNumberFormat: Intl.NumberFormatOptions;
60
+ /** Optional plural rule override; defaults to the built-in language rule. */
61
+ pluralRule?: PluralRule;
62
+ }
63
+
64
+ export type TranslationNode = string | { [key: string]: TranslationNode };
65
+
66
+ export type TranslationNamespace = { [key: string]: TranslationNode };
67
+
68
+ /** App-owned translation resources keyed by locale code. */
69
+ export type TranslationResources = { [localeCode: string]: TranslationNamespace };
70
+
71
+ /**
72
+ * A translation key intentionally resolved at runtime and therefore not
73
+ * discoverable by static source scanning. Declared entries are protected
74
+ * from removal during locale JSON synchronization.
75
+ */
76
+ export interface RuntimeTranslationEntry {
77
+ key: string;
78
+ /** Template text used when no locale resource provides a value. */
79
+ defaultText: string;
80
+ /**
81
+ * Synchronization policy when the key is missing from a locale file:
82
+ * `add` writes the default text, `report` only lists it. Default: `report`.
83
+ */
84
+ policy?: 'add' | 'report';
85
+ /** Owning feature note surfaced in the extraction report. */
86
+ description?: string;
87
+ }
88
+
89
+ export type MissingTranslationPolicy = { mode: 'key' } | { mode: 'placeholder'; text: string };
90
+
91
+ export type InvalidFormatPolicy = { mode: 'placeholder'; text: string } | { mode: 'error' };
92
+
93
+ /** App-injected persistence contract. The package never bundles a backend. */
94
+ export interface LocaleStorageAdapter {
95
+ getItem(key: string): Promise<string | null>;
96
+ setItem(key: string, value: string): Promise<void>;
97
+ removeItem(key: string): Promise<void>;
98
+ /**
99
+ * A synchronous read, for a store that has one (MMKV, rn-core's `KeyValuePort`).
100
+ * With it, `initSync()` resolves the locale before the first render, so the app
101
+ * never paints a frame in the wrong language (PERF-08).
102
+ */
103
+ getItemSync?(key: string): string | null;
104
+ }
105
+
106
+ /** Returns the device locale, or undefined when detection is unavailable. */
107
+ export type DeviceLocaleProvider = () => string | undefined;
108
+
109
+ export interface I18nConfig {
110
+ locales: LocaleConfig[];
111
+ /** Recovery locale when persistence or device detection cannot decide. */
112
+ primaryLocale: string;
113
+ resources: TranslationResources;
114
+ runtimeEntries?: RuntimeTranslationEntry[];
115
+ /** Persist the selected locale across launches. Default: true. */
116
+ persistLocale?: boolean;
117
+ /** Required when `persistLocale` is not disabled. */
118
+ storage?: LocaleStorageAdapter;
119
+ /**
120
+ * Receives every diagnostic: missing translations and variables, fallbacks,
121
+ * invalid formats, persistence failures. Defaults to `console.warn` in
122
+ * development builds only. Diagnostics also reach rn-core's SDK sinks.
123
+ */
124
+ onDiagnostic?: I18nDiagnosticHandler;
125
+ deviceLocale?: DeviceLocaleProvider;
126
+ /** Default: `{ mode: 'key' }`. */
127
+ missingTranslation?: MissingTranslationPolicy;
128
+ /** Default: `{ mode: 'placeholder', text: '—' }`. */
129
+ invalidFormat?: InvalidFormatPolicy;
130
+ }
131
+
132
+ export type LocaleSelectionSource =
133
+ 'persisted' | 'device' | 'languageMatch' | 'primaryFallback' | 'runtimeSwitch';
134
+
135
+ export interface ActiveLocalePreference {
136
+ code: string;
137
+ source: LocaleSelectionSource;
138
+ /** True when an unsupported persisted locale was discarded. */
139
+ recovered: boolean;
140
+ }
141
+
142
+ export type TranslateVars = Record<string, string | number>;
143
+
144
+ export interface TranslateOptions {
145
+ /** Selects the plural variant and is exposed as the `count` variable. */
146
+ count?: number;
147
+ }
148
+
149
+ // Kept importable from here for compatibility; the errors live in config/errors.ts (I18N-14).
150
+ export { I18nConfigError } from './errors';