@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/src/i18n.ts
CHANGED
|
@@ -1,222 +1,196 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ActiveLocalePreference,
|
|
3
|
-
I18nConfig,
|
|
4
|
-
InvalidFormatPolicy,
|
|
5
|
-
LocaleConfig,
|
|
6
|
-
MissingTranslationPolicy,
|
|
7
|
-
RuntimeTranslationEntry,
|
|
8
|
-
TranslateOptions,
|
|
9
|
-
TranslateVars,
|
|
10
|
-
UnitId,
|
|
11
|
-
} from './config/types';
|
|
12
|
-
import { validateConfig } from './config/validateConfig';
|
|
13
|
-
import { createActiveLocaleStore } from './locale/activeLocale';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from './
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
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
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
},
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
value,
|
|
198
|
-
locale.code,
|
|
199
|
-
locale.defaultUnit,
|
|
200
|
-
locale.defaultNumberFormat,
|
|
201
|
-
options,
|
|
202
|
-
invalidFormat,
|
|
203
|
-
);
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
convertUnit(value, from, to) {
|
|
207
|
-
return convertUnitValue(value, from, to);
|
|
208
|
-
},
|
|
209
|
-
|
|
210
|
-
registerRuntimeEntries(entries) {
|
|
211
|
-
registerRuntimeEntries(runtimeEntries, entries);
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
subscribe(listener) {
|
|
215
|
-
return store.subscribe(listener);
|
|
216
|
-
},
|
|
217
|
-
|
|
218
|
-
getVersion() {
|
|
219
|
-
return store.getVersion();
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
ActiveLocalePreference,
|
|
3
|
+
I18nConfig,
|
|
4
|
+
InvalidFormatPolicy,
|
|
5
|
+
LocaleConfig,
|
|
6
|
+
MissingTranslationPolicy,
|
|
7
|
+
RuntimeTranslationEntry,
|
|
8
|
+
TranslateOptions,
|
|
9
|
+
TranslateVars,
|
|
10
|
+
UnitId,
|
|
11
|
+
} from './config/types';
|
|
12
|
+
import { validateConfig } from './config/validateConfig';
|
|
13
|
+
import { createActiveLocaleStore } from './locale/activeLocale';
|
|
14
|
+
import { defaultDeviceLocaleProvider, matchSupportedLocale } from './locale/deviceLocale';
|
|
15
|
+
import { readPersistedLocale, writePersistedLocale } from './locale/persistence';
|
|
16
|
+
import { translate } from './translation/resolver';
|
|
17
|
+
import type { ResolverContext } from './translation/resolver';
|
|
18
|
+
import { createRuntimeRegistry, registerRuntimeEntries } from './translation/runtimeKeys';
|
|
19
|
+
import { formatCurrencyValue } from './format/currency';
|
|
20
|
+
import { formatDateValue } from './format/date';
|
|
21
|
+
import { formatNumberValue } from './format/number';
|
|
22
|
+
import { KNOWN_UNIT_IDS, convertUnitValue, formatUnitValue } from './format/units';
|
|
23
|
+
import type { UnitFormatOptions } from './format/units';
|
|
24
|
+
|
|
25
|
+
export interface I18n<TKey extends string = string> {
|
|
26
|
+
/** Resolves the initial locale: persisted → device → primary fallback. */
|
|
27
|
+
init(): Promise<void>;
|
|
28
|
+
t(key: TKey, vars?: TranslateVars, options?: TranslateOptions): string;
|
|
29
|
+
getLocale(): ActiveLocalePreference;
|
|
30
|
+
/** Switches at runtime without restart and persists best-effort. */
|
|
31
|
+
setLocale(code: string): Promise<void>;
|
|
32
|
+
getSupportedLocales(): readonly LocaleConfig[];
|
|
33
|
+
formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
|
|
34
|
+
formatDate(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
|
|
35
|
+
formatCurrency(value: number, currency?: string, options?: Intl.NumberFormatOptions): string;
|
|
36
|
+
formatUnit(value: number, options?: UnitFormatOptions): string;
|
|
37
|
+
convertUnit(value: number, from: UnitId, to: UnitId): number;
|
|
38
|
+
registerRuntimeEntries(entries: readonly RuntimeTranslationEntry[]): void;
|
|
39
|
+
subscribe(listener: () => void): () => void;
|
|
40
|
+
getVersion(): number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_MISSING: MissingTranslationPolicy = { mode: 'key' };
|
|
44
|
+
const DEFAULT_INVALID: InvalidFormatPolicy = { mode: 'placeholder', text: '—' };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates an I18n instance from an app-owned configuration. Validation runs
|
|
48
|
+
* eagerly and throws an I18nConfigError before any localized content can be
|
|
49
|
+
* served (FR-003). Pass the app's generated key type for typed lookups:
|
|
50
|
+
* `createI18n<AppI18nKey>(config)`.
|
|
51
|
+
*/
|
|
52
|
+
export function createI18n<TKey extends string = string>(config: I18nConfig): I18n<TKey> {
|
|
53
|
+
validateConfig(config, KNOWN_UNIT_IDS);
|
|
54
|
+
|
|
55
|
+
const locales: readonly LocaleConfig[] = [...config.locales];
|
|
56
|
+
const localesByCode = new Map(locales.map(locale => [locale.code, locale]));
|
|
57
|
+
const missingTranslation = config.missingTranslation ?? DEFAULT_MISSING;
|
|
58
|
+
const invalidFormat = config.invalidFormat ?? DEFAULT_INVALID;
|
|
59
|
+
const persistenceEnabled = config.persistLocale !== false && !!config.storage;
|
|
60
|
+
const runtimeEntries = createRuntimeRegistry(config.runtimeEntries);
|
|
61
|
+
|
|
62
|
+
const store = createActiveLocaleStore({
|
|
63
|
+
code: config.primaryLocale,
|
|
64
|
+
source: 'primaryFallback',
|
|
65
|
+
recovered: false,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const resolverContext: ResolverContext = {
|
|
69
|
+
localesByCode,
|
|
70
|
+
resources: config.resources,
|
|
71
|
+
runtimeEntries,
|
|
72
|
+
missingTranslation,
|
|
73
|
+
warned: new Set(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const activeLocale = (): LocaleConfig => {
|
|
77
|
+
const locale = localesByCode.get(store.get().code);
|
|
78
|
+
if (!locale) {
|
|
79
|
+
// Unreachable after validation; guards against external mutation.
|
|
80
|
+
throw new Error(`[rn-i18n] Active locale "${store.get().code}" is not configured.`);
|
|
81
|
+
}
|
|
82
|
+
return locale;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
async init() {
|
|
87
|
+
if (persistenceEnabled && config.storage) {
|
|
88
|
+
const persisted = await readPersistedLocale(config.storage);
|
|
89
|
+
if (persisted) {
|
|
90
|
+
if (localesByCode.has(persisted)) {
|
|
91
|
+
store.set({ code: persisted, source: 'persisted', recovered: false });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
console.warn(
|
|
95
|
+
`[rn-i18n] Persisted locale "${persisted}" is no longer supported; recovering to "${config.primaryLocale}".`,
|
|
96
|
+
);
|
|
97
|
+
store.set({
|
|
98
|
+
code: config.primaryLocale,
|
|
99
|
+
source: 'primaryFallback',
|
|
100
|
+
recovered: true,
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const provider = config.deviceLocale ?? defaultDeviceLocaleProvider;
|
|
106
|
+
const match = matchSupportedLocale(provider(), locales);
|
|
107
|
+
store.set(
|
|
108
|
+
match
|
|
109
|
+
? { code: match.code, source: match.source, recovered: false }
|
|
110
|
+
: {
|
|
111
|
+
code: config.primaryLocale,
|
|
112
|
+
source: 'primaryFallback',
|
|
113
|
+
recovered: false,
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
t(key, vars, options) {
|
|
119
|
+
return translate(resolverContext, store.get().code, key, vars, options);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
getLocale() {
|
|
123
|
+
return store.get();
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async setLocale(code) {
|
|
127
|
+
if (!localesByCode.has(code)) {
|
|
128
|
+
throw new Error(`[rn-i18n] Cannot switch to unsupported locale "${code}".`);
|
|
129
|
+
}
|
|
130
|
+
store.set({ code, source: 'runtimeSwitch', recovered: false });
|
|
131
|
+
if (persistenceEnabled && config.storage) {
|
|
132
|
+
await writePersistedLocale(config.storage, code);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
getSupportedLocales() {
|
|
137
|
+
return locales;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
formatNumber(value, options) {
|
|
141
|
+
const locale = activeLocale();
|
|
142
|
+
return formatNumberValue(
|
|
143
|
+
value,
|
|
144
|
+
locale.code,
|
|
145
|
+
locale.defaultNumberFormat,
|
|
146
|
+
options,
|
|
147
|
+
invalidFormat,
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
formatDate(value, options) {
|
|
152
|
+
const locale = activeLocale();
|
|
153
|
+
return formatDateValue(value, locale.code, locale.defaultDateFormat, options, invalidFormat);
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
formatCurrency(value, currency, options) {
|
|
157
|
+
const locale = activeLocale();
|
|
158
|
+
return formatCurrencyValue(
|
|
159
|
+
value,
|
|
160
|
+
locale.code,
|
|
161
|
+
locale.defaultCurrency,
|
|
162
|
+
currency,
|
|
163
|
+
options,
|
|
164
|
+
invalidFormat,
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
formatUnit(value, options) {
|
|
169
|
+
const locale = activeLocale();
|
|
170
|
+
return formatUnitValue(
|
|
171
|
+
value,
|
|
172
|
+
locale.code,
|
|
173
|
+
locale.defaultUnit,
|
|
174
|
+
locale.defaultNumberFormat,
|
|
175
|
+
options,
|
|
176
|
+
invalidFormat,
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
convertUnit(value, from, to) {
|
|
181
|
+
return convertUnitValue(value, from, to);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
registerRuntimeEntries(entries) {
|
|
185
|
+
registerRuntimeEntries(runtimeEntries, entries);
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
subscribe(listener) {
|
|
189
|
+
return store.subscribe(listener);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
getVersion() {
|
|
193
|
+
return store.getVersion();
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @chipmobilesdk/rn-i18n — reusable localization for ChipMobileSdk-family
|
|
3
|
-
* React Native apps.
|
|
4
|
-
*
|
|
5
|
-
* This is the single React Native entrypoint. Node-only extraction and
|
|
6
|
-
* synchronization tooling is exposed separately via
|
|
7
|
-
* `@chipmobilesdk/rn-i18n/tooling` and must never be imported from here.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export { createI18n } from './i18n';
|
|
11
|
-
export type { I18n } from './i18n';
|
|
12
|
-
|
|
13
|
-
export { I18nConfigError } from './config/types';
|
|
14
|
-
export type {
|
|
15
|
-
ActiveLocalePreference,
|
|
16
|
-
DeviceLocaleProvider,
|
|
17
|
-
I18nConfig,
|
|
18
|
-
InvalidFormatPolicy,
|
|
19
|
-
LocaleConfig,
|
|
20
|
-
LocaleDirection,
|
|
21
|
-
LocaleSelectionSource,
|
|
22
|
-
LocaleStorageAdapter,
|
|
23
|
-
MissingTranslationPolicy,
|
|
24
|
-
PluralCategory,
|
|
25
|
-
PluralRule,
|
|
26
|
-
RuntimeTranslationEntry,
|
|
27
|
-
TranslateOptions,
|
|
28
|
-
TranslateVars,
|
|
29
|
-
TranslationNamespace,
|
|
30
|
-
TranslationNode,
|
|
31
|
-
TranslationResources,
|
|
32
|
-
UnitDimension,
|
|
33
|
-
UnitId,
|
|
34
|
-
} from './config/types';
|
|
35
|
-
|
|
36
|
-
export { LOCALE_STORAGE_KEY } from './locale/persistence';
|
|
37
|
-
export { defaultDeviceLocaleProvider } from './locale/deviceLocale';
|
|
38
|
-
|
|
39
|
-
export {
|
|
40
|
-
SUPPORTED_UNITS,
|
|
41
|
-
UnitConversionError,
|
|
42
|
-
convertUnitValue as convertUnit,
|
|
43
|
-
getUnitSymbol,
|
|
44
|
-
} from './format/units';
|
|
45
|
-
export type { UnitFormatOptions } from './format/units';
|
|
46
|
-
|
|
47
|
-
export { I18nProvider } from './react/I18nProvider';
|
|
48
|
-
export type { I18nProviderProps } from './react/I18nProvider';
|
|
49
|
-
export { useLocale, useTranslation } from './react/hooks';
|
|
50
|
-
export type { UseLocaleResult, UseTranslationResult } from './react/hooks';
|
|
1
|
+
/**
|
|
2
|
+
* @chipmobilesdk/rn-i18n — reusable localization for ChipMobileSdk-family
|
|
3
|
+
* React Native apps.
|
|
4
|
+
*
|
|
5
|
+
* This is the single React Native entrypoint. Node-only extraction and
|
|
6
|
+
* synchronization tooling is exposed separately via
|
|
7
|
+
* `@chipmobilesdk/rn-i18n/tooling` and must never be imported from here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { createI18n } from './i18n';
|
|
11
|
+
export type { I18n } from './i18n';
|
|
12
|
+
|
|
13
|
+
export { I18nConfigError } from './config/types';
|
|
14
|
+
export type {
|
|
15
|
+
ActiveLocalePreference,
|
|
16
|
+
DeviceLocaleProvider,
|
|
17
|
+
I18nConfig,
|
|
18
|
+
InvalidFormatPolicy,
|
|
19
|
+
LocaleConfig,
|
|
20
|
+
LocaleDirection,
|
|
21
|
+
LocaleSelectionSource,
|
|
22
|
+
LocaleStorageAdapter,
|
|
23
|
+
MissingTranslationPolicy,
|
|
24
|
+
PluralCategory,
|
|
25
|
+
PluralRule,
|
|
26
|
+
RuntimeTranslationEntry,
|
|
27
|
+
TranslateOptions,
|
|
28
|
+
TranslateVars,
|
|
29
|
+
TranslationNamespace,
|
|
30
|
+
TranslationNode,
|
|
31
|
+
TranslationResources,
|
|
32
|
+
UnitDimension,
|
|
33
|
+
UnitId,
|
|
34
|
+
} from './config/types';
|
|
35
|
+
|
|
36
|
+
export { LOCALE_STORAGE_KEY } from './locale/persistence';
|
|
37
|
+
export { defaultDeviceLocaleProvider } from './locale/deviceLocale';
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
SUPPORTED_UNITS,
|
|
41
|
+
UnitConversionError,
|
|
42
|
+
convertUnitValue as convertUnit,
|
|
43
|
+
getUnitSymbol,
|
|
44
|
+
} from './format/units';
|
|
45
|
+
export type { UnitFormatOptions } from './format/units';
|
|
46
|
+
|
|
47
|
+
export { I18nProvider } from './react/I18nProvider';
|
|
48
|
+
export type { I18nProviderProps } from './react/I18nProvider';
|
|
49
|
+
export { useLocale, useTranslation } from './react/hooks';
|
|
50
|
+
export type { UseLocaleResult, UseTranslationResult } from './react/hooks';
|
|
@@ -1,45 +1,46 @@
|
|
|
1
|
-
import type { ActiveLocalePreference } from '../config/types';
|
|
2
|
-
|
|
3
|
-
export interface ActiveLocaleStore {
|
|
4
|
-
get(): ActiveLocalePreference;
|
|
5
|
-
set(preference: ActiveLocalePreference): void;
|
|
6
|
-
subscribe(listener: () => void): () => void;
|
|
7
|
-
/** Monotonic change counter consumed by useSyncExternalStore. */
|
|
8
|
-
getVersion(): number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function createActiveLocaleStore(
|
|
12
|
-
initial
|
|
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
|
-
}
|
|
1
|
+
import type { ActiveLocalePreference } from '../config/types';
|
|
2
|
+
|
|
3
|
+
export interface ActiveLocaleStore {
|
|
4
|
+
get(): ActiveLocalePreference;
|
|
5
|
+
set(preference: ActiveLocalePreference): void;
|
|
6
|
+
subscribe(listener: () => void): () => void;
|
|
7
|
+
/** Monotonic change counter consumed by useSyncExternalStore. */
|
|
8
|
+
getVersion(): number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createActiveLocaleStore(initial: ActiveLocalePreference): ActiveLocaleStore {
|
|
12
|
+
let preference = initial;
|
|
13
|
+
let version = 0;
|
|
14
|
+
const listeners = new Set<() => void>();
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
get: () => preference,
|
|
18
|
+
set(next) {
|
|
19
|
+
preference = next;
|
|
20
|
+
version += 1;
|
|
21
|
+
// Snapshot, and isolate each listener (CORE-03). One that threw used to
|
|
22
|
+
// skip every listener after it — useSyncExternalStore subscribers
|
|
23
|
+
// included — and made setLocale reject after the switch but before the
|
|
24
|
+
// choice was persisted, so the app showed one locale and stored another.
|
|
25
|
+
for (const listener of [...listeners]) {
|
|
26
|
+
// Unsubscribed by an earlier listener during this notification.
|
|
27
|
+
if (!listeners.has(listener)) continue;
|
|
28
|
+
try {
|
|
29
|
+
listener();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn(
|
|
32
|
+
'[rn-i18n] A locale listener threw; the locale change still applied.',
|
|
33
|
+
error,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
subscribe(listener) {
|
|
39
|
+
listeners.add(listener);
|
|
40
|
+
return () => {
|
|
41
|
+
listeners.delete(listener);
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
getVersion: () => version,
|
|
45
|
+
};
|
|
46
|
+
}
|