@chipmobilesdk/rn-i18n 0.2.0 → 0.4.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.
- package/CHANGELOG.md +173 -0
- package/README.md +48 -17
- package/lib/commonjs/config/catalog.js +29 -0
- package/lib/commonjs/config/catalog.js.map +1 -0
- package/lib/commonjs/config/diagnostics.js +56 -0
- package/lib/commonjs/config/diagnostics.js.map +1 -0
- package/lib/commonjs/config/errors.js +35 -0
- package/lib/commonjs/config/errors.js.map +1 -0
- package/lib/commonjs/config/types.js +13 -0
- package/lib/commonjs/config/types.js.map +1 -0
- package/lib/commonjs/config/validateConfig.js +134 -0
- package/lib/commonjs/config/validateConfig.js.map +1 -0
- package/lib/commonjs/format/currency.js +35 -0
- package/lib/commonjs/format/currency.js.map +1 -0
- package/lib/commonjs/format/date.js +36 -0
- package/lib/commonjs/format/date.js.map +1 -0
- package/lib/commonjs/format/invalid.js +24 -0
- package/lib/commonjs/format/invalid.js.map +1 -0
- package/lib/commonjs/format/number.js +29 -0
- package/lib/commonjs/format/number.js.map +1 -0
- package/lib/commonjs/format/units.js +184 -0
- package/lib/commonjs/format/units.js.map +1 -0
- package/lib/commonjs/i18n.js +234 -0
- package/lib/commonjs/i18n.js.map +1 -0
- package/lib/commonjs/index.js +93 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/locale/activeLocale.js +37 -0
- package/lib/commonjs/locale/activeLocale.js.map +1 -0
- package/lib/commonjs/locale/deviceLocale.js +52 -0
- package/lib/commonjs/locale/deviceLocale.js.map +1 -0
- package/lib/commonjs/locale/keyValueStorage.js +29 -0
- package/lib/commonjs/locale/keyValueStorage.js.map +1 -0
- package/lib/commonjs/locale/persistence.js +74 -0
- package/lib/commonjs/locale/persistence.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/react/I18nProvider.js +36 -0
- package/lib/commonjs/react/I18nProvider.js.map +1 -0
- package/lib/commonjs/react/hooks.js +47 -0
- package/lib/commonjs/react/hooks.js.map +1 -0
- package/lib/commonjs/translation/interpolate.js +24 -0
- package/lib/commonjs/translation/interpolate.js.map +1 -0
- package/lib/commonjs/translation/plurals.js +122 -0
- package/lib/commonjs/translation/plurals.js.map +1 -0
- package/lib/commonjs/translation/resolver.js +124 -0
- package/lib/commonjs/translation/resolver.js.map +1 -0
- package/lib/commonjs/translation/runtimeKeys.js +55 -0
- package/lib/commonjs/translation/runtimeKeys.js.map +1 -0
- package/lib/module/config/catalog.js +25 -0
- package/lib/module/config/catalog.js.map +1 -0
- package/lib/module/config/diagnostics.js +51 -0
- package/lib/module/config/diagnostics.js.map +1 -0
- package/lib/module/config/errors.js +30 -0
- package/lib/module/config/errors.js.map +1 -0
- package/lib/module/config/types.js +26 -0
- package/lib/module/config/types.js.map +1 -0
- package/lib/module/config/validateConfig.js +131 -0
- package/lib/module/config/validateConfig.js.map +1 -0
- package/lib/module/format/currency.js +32 -0
- package/lib/module/format/currency.js.map +1 -0
- package/lib/module/format/date.js +33 -0
- package/lib/module/format/date.js.map +1 -0
- package/lib/module/format/invalid.js +21 -0
- package/lib/module/format/invalid.js.map +1 -0
- package/lib/module/format/number.js +26 -0
- package/lib/module/format/number.js.map +1 -0
- package/lib/module/format/units.js +171 -0
- package/lib/module/format/units.js.map +1 -0
- package/lib/module/i18n.js +230 -0
- package/lib/module/i18n.js.map +1 -0
- package/lib/module/index.js +23 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/locale/activeLocale.js +33 -0
- package/lib/module/locale/activeLocale.js.map +1 -0
- package/lib/module/locale/deviceLocale.js +46 -0
- package/lib/module/locale/deviceLocale.js.map +1 -0
- package/lib/module/locale/keyValueStorage.js +25 -0
- package/lib/module/locale/keyValueStorage.js.map +1 -0
- package/lib/module/locale/persistence.js +66 -0
- package/lib/module/locale/persistence.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/react/I18nProvider.js +29 -0
- package/lib/module/react/I18nProvider.js.map +1 -0
- package/lib/module/react/hooks.js +42 -0
- package/lib/module/react/hooks.js.map +1 -0
- package/lib/module/translation/interpolate.js +20 -0
- package/lib/module/translation/interpolate.js.map +1 -0
- package/lib/module/translation/plurals.js +114 -0
- package/lib/module/translation/plurals.js.map +1 -0
- package/lib/module/translation/resolver.js +118 -0
- package/lib/module/translation/resolver.js.map +1 -0
- package/lib/module/translation/runtimeKeys.js +50 -0
- package/lib/module/translation/runtimeKeys.js.map +1 -0
- package/lib/typescript/commonjs/config/catalog.d.ts +18 -0
- package/lib/typescript/commonjs/config/catalog.d.ts.map +1 -0
- package/lib/typescript/commonjs/config/diagnostics.d.ts +21 -0
- package/lib/typescript/commonjs/config/diagnostics.d.ts.map +1 -0
- package/lib/typescript/commonjs/config/errors.d.ts +16 -0
- package/lib/typescript/commonjs/config/errors.d.ts.map +1 -0
- package/lib/typescript/commonjs/config/types.d.ts +127 -0
- package/lib/typescript/commonjs/config/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/config/validateConfig.d.ts +8 -0
- package/lib/typescript/commonjs/config/validateConfig.d.ts.map +1 -0
- package/lib/typescript/commonjs/format/currency.d.ts +12 -0
- package/lib/typescript/commonjs/format/currency.d.ts.map +1 -0
- package/lib/typescript/commonjs/format/date.d.ts +14 -0
- package/lib/typescript/commonjs/format/date.d.ts.map +1 -0
- package/lib/typescript/commonjs/format/invalid.d.ts +12 -0
- package/lib/typescript/commonjs/format/invalid.d.ts.map +1 -0
- package/lib/typescript/commonjs/format/number.d.ts +8 -0
- package/lib/typescript/commonjs/format/number.d.ts.map +1 -0
- package/lib/typescript/commonjs/format/units.d.ts +23 -0
- package/lib/typescript/commonjs/format/units.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n.d.ts +46 -0
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +27 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/locale/activeLocale.d.ts +17 -0
- package/lib/typescript/commonjs/locale/activeLocale.d.ts.map +1 -0
- package/lib/typescript/commonjs/locale/deviceLocale.d.ts +19 -0
- package/lib/typescript/commonjs/locale/deviceLocale.d.ts.map +1 -0
- package/lib/typescript/commonjs/locale/keyValueStorage.d.ts +13 -0
- package/lib/typescript/commonjs/locale/keyValueStorage.d.ts.map +1 -0
- package/lib/typescript/commonjs/locale/persistence.d.ts +24 -0
- package/lib/typescript/commonjs/locale/persistence.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/react/I18nProvider.d.ts +16 -0
- package/lib/typescript/commonjs/react/I18nProvider.d.ts.map +1 -0
- package/lib/typescript/commonjs/react/hooks.d.ts +21 -0
- package/lib/typescript/commonjs/react/hooks.d.ts.map +1 -0
- package/lib/typescript/commonjs/translation/interpolate.d.ts +8 -0
- package/lib/typescript/commonjs/translation/interpolate.d.ts.map +1 -0
- package/lib/typescript/commonjs/translation/plurals.d.ts +13 -0
- package/lib/typescript/commonjs/translation/plurals.d.ts.map +1 -0
- package/lib/typescript/commonjs/translation/resolver.d.ts +24 -0
- package/lib/typescript/commonjs/translation/resolver.d.ts.map +1 -0
- package/lib/typescript/commonjs/translation/runtimeKeys.d.ts +16 -0
- package/lib/typescript/commonjs/translation/runtimeKeys.d.ts.map +1 -0
- package/lib/typescript/module/config/catalog.d.ts +18 -0
- package/lib/typescript/module/config/catalog.d.ts.map +1 -0
- package/lib/typescript/module/config/diagnostics.d.ts +21 -0
- package/lib/typescript/module/config/diagnostics.d.ts.map +1 -0
- package/lib/typescript/module/config/errors.d.ts +16 -0
- package/lib/typescript/module/config/errors.d.ts.map +1 -0
- package/lib/typescript/module/config/types.d.ts +127 -0
- package/lib/typescript/module/config/types.d.ts.map +1 -0
- package/lib/typescript/module/config/validateConfig.d.ts +8 -0
- package/lib/typescript/module/config/validateConfig.d.ts.map +1 -0
- package/lib/typescript/module/format/currency.d.ts +12 -0
- package/lib/typescript/module/format/currency.d.ts.map +1 -0
- package/lib/typescript/module/format/date.d.ts +14 -0
- package/lib/typescript/module/format/date.d.ts.map +1 -0
- package/lib/typescript/module/format/invalid.d.ts +12 -0
- package/lib/typescript/module/format/invalid.d.ts.map +1 -0
- package/lib/typescript/module/format/number.d.ts +8 -0
- package/lib/typescript/module/format/number.d.ts.map +1 -0
- package/lib/typescript/module/format/units.d.ts +23 -0
- package/lib/typescript/module/format/units.d.ts.map +1 -0
- package/lib/typescript/module/i18n.d.ts +46 -0
- package/lib/typescript/module/i18n.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +27 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/locale/activeLocale.d.ts +17 -0
- package/lib/typescript/module/locale/activeLocale.d.ts.map +1 -0
- package/lib/typescript/module/locale/deviceLocale.d.ts +19 -0
- package/lib/typescript/module/locale/deviceLocale.d.ts.map +1 -0
- package/lib/typescript/module/locale/keyValueStorage.d.ts +13 -0
- package/lib/typescript/module/locale/keyValueStorage.d.ts.map +1 -0
- package/lib/typescript/module/locale/persistence.d.ts +24 -0
- package/lib/typescript/module/locale/persistence.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/react/I18nProvider.d.ts +16 -0
- package/lib/typescript/module/react/I18nProvider.d.ts.map +1 -0
- package/lib/typescript/module/react/hooks.d.ts +21 -0
- package/lib/typescript/module/react/hooks.d.ts.map +1 -0
- package/lib/typescript/module/translation/interpolate.d.ts +8 -0
- package/lib/typescript/module/translation/interpolate.d.ts.map +1 -0
- package/lib/typescript/module/translation/plurals.d.ts +13 -0
- package/lib/typescript/module/translation/plurals.d.ts.map +1 -0
- package/lib/typescript/module/translation/resolver.d.ts +24 -0
- package/lib/typescript/module/translation/resolver.d.ts.map +1 -0
- package/lib/typescript/module/translation/runtimeKeys.d.ts +16 -0
- package/lib/typescript/module/translation/runtimeKeys.d.ts.map +1 -0
- package/package.json +41 -18
- package/skills/sdk-rn-i18n/SKILL.md +27 -0
- package/src/config/diagnostics.ts +2 -0
- package/src/format/currency.ts +5 -0
- package/src/format/date.ts +10 -4
- package/src/i18n.ts +93 -9
- package/src/index.ts +5 -3
- package/src/locale/activeLocale.ts +10 -0
- package/src/locale/persistence.ts +19 -0
- package/src/translation/plurals.ts +74 -5
- package/src/translation/runtimeKeys.ts +28 -1
- package/src/tooling/cli.ts +0 -76
- package/src/tooling/generateKeys.ts +0 -62
- package/src/tooling/index.ts +0 -27
- package/src/tooling/report.ts +0 -109
- package/src/tooling/runner.ts +0 -111
- package/src/tooling/scan.ts +0 -193
- package/src/tooling/sync.ts +0 -193
- package/src/tooling/types.ts +0 -119
package/src/i18n.ts
CHANGED
|
@@ -15,10 +15,12 @@ import { validateConfig } from './config/validateConfig';
|
|
|
15
15
|
import { createActiveLocaleStore } from './locale/activeLocale';
|
|
16
16
|
import { defaultDeviceLocaleProvider, matchSupportedLocale } from './locale/deviceLocale';
|
|
17
17
|
import {
|
|
18
|
+
clearPersistedLocale,
|
|
18
19
|
readPersistedLocale,
|
|
19
20
|
readPersistedLocaleSync,
|
|
20
21
|
writePersistedLocale,
|
|
21
22
|
} from './locale/persistence';
|
|
23
|
+
import { hasPluralRule } from './translation/plurals';
|
|
22
24
|
import { translate } from './translation/resolver';
|
|
23
25
|
import type { ResolverContext } from './translation/resolver';
|
|
24
26
|
import { createRuntimeRegistry, registerRuntimeEntries } from './translation/runtimeKeys';
|
|
@@ -42,9 +44,23 @@ export interface I18n<TKey extends string = string> {
|
|
|
42
44
|
getLocale(): ActiveLocalePreference;
|
|
43
45
|
/** Switches at runtime without restart and persists best-effort. */
|
|
44
46
|
setLocale(code: string): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Forgets the stored choice and follows the device again — the "System
|
|
49
|
+
* language" option a settings screen offers (I18N-17). Resolves to the
|
|
50
|
+
* locale now active.
|
|
51
|
+
*/
|
|
52
|
+
useDeviceLocale(): Promise<ActiveLocalePreference>;
|
|
45
53
|
getSupportedLocales(): readonly LocaleConfig[];
|
|
46
54
|
formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
|
|
47
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Options are merged over the locale's default (`{ month: 'long' }` keeps the
|
|
57
|
+
* year and day). `replaceDefaults` uses only what is passed.
|
|
58
|
+
*/
|
|
59
|
+
formatDate(
|
|
60
|
+
value: Date | number,
|
|
61
|
+
options?: Intl.DateTimeFormatOptions,
|
|
62
|
+
formatting?: { replaceDefaults?: boolean },
|
|
63
|
+
): string;
|
|
48
64
|
formatCurrency(value: number, currency?: string, options?: Intl.NumberFormatOptions): string;
|
|
49
65
|
formatUnit(value: number, options?: UnitFormatOptions): string;
|
|
50
66
|
convertUnit(value: number, from: UnitId, to: UnitId): number;
|
|
@@ -81,6 +97,21 @@ export function createI18n<TKey extends string = string>(config: I18nConfig): I1
|
|
|
81
97
|
report,
|
|
82
98
|
);
|
|
83
99
|
|
|
100
|
+
// A language this package has no plural rule for falls back to
|
|
101
|
+
// `count === 1 ? one : other`, which is wrong for most of the world. Said
|
|
102
|
+
// once per locale, at creation, rather than never (I18N-12).
|
|
103
|
+
for (const locale of locales) {
|
|
104
|
+
if (!hasPluralRule(locale)) {
|
|
105
|
+
report({
|
|
106
|
+
code: 'plural_rule_default',
|
|
107
|
+
locale: locale.code,
|
|
108
|
+
message:
|
|
109
|
+
`No plural rule for "${locale.code}"; falling back to "count === 1 selects one". ` +
|
|
110
|
+
'Declare `pluralRule` on the locale if that is not how the language works.',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
const resolverContext: ResolverContext = {
|
|
85
116
|
localesByCode,
|
|
86
117
|
resources: config.resources,
|
|
@@ -123,13 +154,33 @@ export function createI18n<TKey extends string = string>(config: I18nConfig): I1
|
|
|
123
154
|
);
|
|
124
155
|
};
|
|
125
156
|
|
|
157
|
+
/**
|
|
158
|
+
* `init()` runs once, and loses to the user.
|
|
159
|
+
*
|
|
160
|
+
* It used to be re-run by every caller (StrictMode mounts an effect twice),
|
|
161
|
+
* and the persisted value it had awaited was written in unconditionally — so
|
|
162
|
+
* a locale the user picked, or a deep link set, while the read was in flight
|
|
163
|
+
* was overwritten by the old one (I18N-07).
|
|
164
|
+
*/
|
|
165
|
+
let initialized: Promise<void> | undefined;
|
|
166
|
+
let switched = false;
|
|
167
|
+
|
|
168
|
+
const runInit = async (): Promise<void> => {
|
|
169
|
+
const persisted =
|
|
170
|
+
persistenceEnabled && config.storage
|
|
171
|
+
? await readPersistedLocale(config.storage, report)
|
|
172
|
+
: null;
|
|
173
|
+
if (switched) {
|
|
174
|
+
// The user chose while the read was in flight; their choice stands.
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
resolveInitialLocale(persisted);
|
|
178
|
+
};
|
|
179
|
+
|
|
126
180
|
return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
? await readPersistedLocale(config.storage, report)
|
|
131
|
-
: null,
|
|
132
|
-
);
|
|
181
|
+
init() {
|
|
182
|
+
initialized ??= runInit();
|
|
183
|
+
return initialized;
|
|
133
184
|
},
|
|
134
185
|
|
|
135
186
|
initSync() {
|
|
@@ -162,12 +213,34 @@ export function createI18n<TKey extends string = string>(config: I18nConfig): I1
|
|
|
162
213
|
if (!localesByCode.has(code)) {
|
|
163
214
|
throw new Error(`[rn-i18n] Cannot switch to unsupported locale "${code}".`);
|
|
164
215
|
}
|
|
216
|
+
switched = true;
|
|
217
|
+
const current = store.get();
|
|
218
|
+
if (current.code === code && current.source === 'runtimeSwitch') {
|
|
219
|
+
// Already this locale, by this route: re-rendering the whole tree and
|
|
220
|
+
// rewriting storage would change nothing (I18N-07).
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
165
223
|
store.set({ code, source: 'runtimeSwitch', recovered: false });
|
|
166
224
|
if (persistenceEnabled && config.storage) {
|
|
167
225
|
await writePersistedLocale(config.storage, code, report);
|
|
168
226
|
}
|
|
169
227
|
},
|
|
170
228
|
|
|
229
|
+
async useDeviceLocale() {
|
|
230
|
+
switched = true;
|
|
231
|
+
if (persistenceEnabled && config.storage) {
|
|
232
|
+
await clearPersistedLocale(config.storage, report);
|
|
233
|
+
}
|
|
234
|
+
const provider = config.deviceLocale ?? defaultDeviceLocaleProvider;
|
|
235
|
+
const match = matchSupportedLocale(provider(), locales);
|
|
236
|
+
store.set(
|
|
237
|
+
match
|
|
238
|
+
? { code: match.code, source: match.source, recovered: false }
|
|
239
|
+
: { code: config.primaryLocale, source: 'primaryFallback', recovered: false },
|
|
240
|
+
);
|
|
241
|
+
return store.get();
|
|
242
|
+
},
|
|
243
|
+
|
|
171
244
|
getSupportedLocales() {
|
|
172
245
|
return locales;
|
|
173
246
|
},
|
|
@@ -183,9 +256,16 @@ export function createI18n<TKey extends string = string>(config: I18nConfig): I1
|
|
|
183
256
|
);
|
|
184
257
|
},
|
|
185
258
|
|
|
186
|
-
formatDate(value, options) {
|
|
259
|
+
formatDate(value, options, formatting) {
|
|
187
260
|
const locale = activeLocale();
|
|
188
|
-
return formatDateValue(
|
|
261
|
+
return formatDateValue(
|
|
262
|
+
value,
|
|
263
|
+
locale.code,
|
|
264
|
+
locale.defaultDateFormat,
|
|
265
|
+
options,
|
|
266
|
+
invalidFormat,
|
|
267
|
+
formatting?.replaceDefaults === true,
|
|
268
|
+
);
|
|
189
269
|
},
|
|
190
270
|
|
|
191
271
|
formatCurrency(value, currency, options) {
|
|
@@ -218,6 +298,10 @@ export function createI18n<TKey extends string = string>(config: I18nConfig): I1
|
|
|
218
298
|
|
|
219
299
|
registerRuntimeEntries(entries) {
|
|
220
300
|
registerRuntimeEntries(runtimeEntries, entries);
|
|
301
|
+
// What `t()` answers has changed, so every subscriber has to read again:
|
|
302
|
+
// a feature that registered its entries after mounting used to leave the
|
|
303
|
+
// screen showing the key (I18N-06).
|
|
304
|
+
store.bump();
|
|
221
305
|
},
|
|
222
306
|
|
|
223
307
|
subscribe(listener) {
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* @chipmobilesdk/rn-i18n — reusable localization for ChipMobileSdk-family
|
|
3
3
|
* React Native apps.
|
|
4
4
|
*
|
|
5
|
-
* This is the single React Native
|
|
6
|
-
* synchronization tooling is
|
|
7
|
-
* `@chipmobilesdk/rn-i18n
|
|
5
|
+
* This is the single entrypoint, and the package is React Native code only. The
|
|
6
|
+
* Node-only extraction and synchronization tooling is a package of its own,
|
|
7
|
+
* `@chipmobilesdk/rn-i18n-tooling` (the `chip-i18n` CLI): it used to ship inside
|
|
8
|
+
* this tarball as TypeScript source under `./tooling`, which no consumer could
|
|
9
|
+
* execute.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
export { createI18n } from './i18n';
|
|
@@ -7,6 +7,12 @@ export interface ActiveLocaleStore {
|
|
|
7
7
|
get(): ActiveLocalePreference;
|
|
8
8
|
set(preference: ActiveLocalePreference): void;
|
|
9
9
|
subscribe(listener: () => void): () => void;
|
|
10
|
+
/**
|
|
11
|
+
* Notifies subscribers without changing the locale: what reaches the screen
|
|
12
|
+
* changed, not which language it is in (a runtime entry registered by a
|
|
13
|
+
* lazily loaded feature, say).
|
|
14
|
+
*/
|
|
15
|
+
bump(): void;
|
|
10
16
|
/** Monotonic change counter consumed by useSyncExternalStore. */
|
|
11
17
|
getVersion(): number;
|
|
12
18
|
}
|
|
@@ -36,6 +42,10 @@ export function createActiveLocaleStore(
|
|
|
36
42
|
// app showed one locale and stored another.
|
|
37
43
|
listeners.notify();
|
|
38
44
|
},
|
|
45
|
+
bump() {
|
|
46
|
+
version += 1;
|
|
47
|
+
listeners.notify();
|
|
48
|
+
},
|
|
39
49
|
subscribe: listener => listeners.add(listener),
|
|
40
50
|
getVersion: () => version,
|
|
41
51
|
};
|
|
@@ -57,3 +57,22 @@ export async function writePersistedLocale(
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Forgets the stored choice, so the next launch follows the device again. The
|
|
63
|
+
* adapter's `removeItem` was required by the contract and called by nothing
|
|
64
|
+
* until `useDeviceLocale()` (I18N-17). Best-effort, like the write.
|
|
65
|
+
*/
|
|
66
|
+
export async function clearPersistedLocale(
|
|
67
|
+
storage: LocaleStorageAdapter,
|
|
68
|
+
report: I18nDiagnosticHandler = defaultReporter,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
await storage.removeItem(LOCALE_STORAGE_KEY);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
report({
|
|
74
|
+
code: 'locale_write_failed',
|
|
75
|
+
message: `Failed to forget the persisted locale preference: ${describeError(error)}`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -11,22 +11,91 @@ export const PLURAL_CATEGORIES: readonly PluralCategory[] = [
|
|
|
11
11
|
|
|
12
12
|
const oneOtherRule: PluralRule = count => (count === 1 ? 'one' : 'other');
|
|
13
13
|
const otherOnlyRule: PluralRule = () => 'other';
|
|
14
|
+
/** French and Brazilian Portuguese: 0 and 1 are both singular. */
|
|
15
|
+
const zeroOneRule: PluralRule = count => (count === 0 || count === 1 ? 'one' : 'other');
|
|
16
|
+
|
|
17
|
+
/** Russian, Ukrainian, Polish, Czech and their neighbours (CLDR). */
|
|
18
|
+
const slavicRule: PluralRule = count => {
|
|
19
|
+
if (!Number.isInteger(count)) {
|
|
20
|
+
return 'many';
|
|
21
|
+
}
|
|
22
|
+
const mod10 = Math.abs(count) % 10;
|
|
23
|
+
const mod100 = Math.abs(count) % 100;
|
|
24
|
+
if (mod10 === 1 && mod100 !== 11) {
|
|
25
|
+
return 'one';
|
|
26
|
+
}
|
|
27
|
+
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) {
|
|
28
|
+
return 'few';
|
|
29
|
+
}
|
|
30
|
+
return 'many';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Arabic (CLDR), which uses every category. */
|
|
34
|
+
const arabicRule: PluralRule = count => {
|
|
35
|
+
if (count === 0) return 'zero';
|
|
36
|
+
if (count === 1) return 'one';
|
|
37
|
+
if (count === 2) return 'two';
|
|
38
|
+
const mod100 = Math.abs(count) % 100;
|
|
39
|
+
if (mod100 >= 3 && mod100 <= 10) return 'few';
|
|
40
|
+
if (mod100 >= 11 && mod100 <= 99) return 'many';
|
|
41
|
+
return 'other';
|
|
42
|
+
};
|
|
14
43
|
|
|
15
44
|
/**
|
|
16
|
-
* Package-owned plural rules. Intl.PluralRules is not
|
|
17
|
-
* so rules are resolved from configuration to keep
|
|
18
|
-
* devices and test environments.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
45
|
+
* Package-owned plural rules, a compact subset of CLDR. Intl.PluralRules is not
|
|
46
|
+
* implemented by Hermes, so rules are resolved from configuration to keep
|
|
47
|
+
* behavior identical across devices and test environments.
|
|
48
|
+
*
|
|
49
|
+
* A language that is not here falls back to `count === 1 ? one : other`, which
|
|
50
|
+
* is wrong for most of the world — Korean and Thai have no singular, French
|
|
51
|
+
* calls 0 singular, Russian has `few` and `many` — so the fallback now reports
|
|
52
|
+
* a diagnostic once per locale rather than being silently applied (I18N-12).
|
|
21
53
|
*/
|
|
22
54
|
const BUILT_IN_RULES: Record<string, PluralRule> = {
|
|
55
|
+
// one/other
|
|
23
56
|
en: oneOtherRule,
|
|
57
|
+
de: oneOtherRule,
|
|
58
|
+
es: oneOtherRule,
|
|
59
|
+
it: oneOtherRule,
|
|
60
|
+
nl: oneOtherRule,
|
|
61
|
+
sv: oneOtherRule,
|
|
62
|
+
da: oneOtherRule,
|
|
63
|
+
nb: oneOtherRule,
|
|
64
|
+
fi: oneOtherRule,
|
|
65
|
+
el: oneOtherRule,
|
|
66
|
+
he: oneOtherRule,
|
|
67
|
+
// other only
|
|
24
68
|
ja: otherOnlyRule,
|
|
25
69
|
vi: otherOnlyRule,
|
|
70
|
+
ko: otherOnlyRule,
|
|
71
|
+
zh: otherOnlyRule,
|
|
72
|
+
th: otherOnlyRule,
|
|
73
|
+
id: otherOnlyRule,
|
|
74
|
+
ms: otherOnlyRule,
|
|
75
|
+
// 0 and 1 are singular
|
|
76
|
+
fr: zeroOneRule,
|
|
77
|
+
pt: zeroOneRule,
|
|
78
|
+
// Slavic
|
|
79
|
+
ru: slavicRule,
|
|
80
|
+
uk: slavicRule,
|
|
81
|
+
pl: slavicRule,
|
|
82
|
+
cs: slavicRule,
|
|
83
|
+
sk: slavicRule,
|
|
84
|
+
// Every category
|
|
85
|
+
ar: arabicRule,
|
|
26
86
|
};
|
|
27
87
|
|
|
28
88
|
export const DEFAULT_PLURAL_RULE: PluralRule = oneOtherRule;
|
|
29
89
|
|
|
90
|
+
/** True when this package has a rule for the language, or the locale brought one. */
|
|
91
|
+
export function hasPluralRule(locale: LocaleConfig): boolean {
|
|
92
|
+
if (locale.pluralRule) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const language = locale.code.split('-')[0]?.toLowerCase() ?? '';
|
|
96
|
+
return language in BUILT_IN_RULES;
|
|
97
|
+
}
|
|
98
|
+
|
|
30
99
|
export function resolvePluralRule(locale: LocaleConfig): PluralRule {
|
|
31
100
|
if (locale.pluralRule) {
|
|
32
101
|
return locale.pluralRule;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { I18nConfigError, type RuntimeTranslationEntry } from '../config/types';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Registry of declared runtime-only translation entries. Entries participate
|
|
@@ -17,10 +17,37 @@ export function createRuntimeRegistry(
|
|
|
17
17
|
return registry;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The same checks the configured entries get (I18N-06): an entry registered at
|
|
22
|
+
* runtime by a lazily loaded feature used to be written in unvalidated, so a
|
|
23
|
+
* missing key or a non-string default surfaced only when a screen rendered it.
|
|
24
|
+
*/
|
|
25
|
+
export function validateRuntimeEntries(entries: readonly RuntimeTranslationEntry[]): void {
|
|
26
|
+
const issues: string[] = [];
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (!entry?.key) {
|
|
30
|
+
issues.push('Runtime-only entries must declare a non-empty key.');
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (seen.has(entry.key)) {
|
|
34
|
+
issues.push(`Duplicate runtime-only entry "${entry.key}".`);
|
|
35
|
+
}
|
|
36
|
+
seen.add(entry.key);
|
|
37
|
+
if (typeof entry.defaultText !== 'string') {
|
|
38
|
+
issues.push(`Runtime-only entry "${entry.key}" must declare default text.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (issues.length > 0) {
|
|
42
|
+
throw new I18nConfigError(issues);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
20
46
|
export function registerRuntimeEntries(
|
|
21
47
|
registry: RuntimeEntryRegistry,
|
|
22
48
|
entries: readonly RuntimeTranslationEntry[],
|
|
23
49
|
): void {
|
|
50
|
+
validateRuntimeEntries(entries);
|
|
24
51
|
for (const entry of entries) {
|
|
25
52
|
registry.set(entry.key, entry);
|
|
26
53
|
}
|
package/src/tooling/cli.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extraction/synchronization CLI. Node-only; run through the root npm
|
|
3
|
-
* script under Node >= 22.11 with TypeScript type stripping:
|
|
4
|
-
*
|
|
5
|
-
* npm run i18n:sync
|
|
6
|
-
* node --experimental-strip-types packages/rn-i18n/src/tooling/cli.ts --config i18n-sync.config.json
|
|
7
|
-
*
|
|
8
|
-
* Exit codes: 0 success or warnings, 1 blocking findings (conflicts or
|
|
9
|
-
* malformed locale files), 2 invalid configuration or internal error.
|
|
10
|
-
*/
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
import process from 'node:process';
|
|
14
|
-
import { renderReport } from './report.ts';
|
|
15
|
-
import { SyncConfigError, runSync } from './runner.ts';
|
|
16
|
-
import type { SyncConfig } from './types.ts';
|
|
17
|
-
|
|
18
|
-
function parseArgs(argv: readonly string[]): { configPath: string } {
|
|
19
|
-
let configPath = 'i18n-sync.config.json';
|
|
20
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
21
|
-
if (argv[index] === '--config' && argv[index + 1]) {
|
|
22
|
-
configPath = argv[index + 1];
|
|
23
|
-
index += 1;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return { configPath };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function main(): Promise<number> {
|
|
30
|
-
const rootDir = process.cwd();
|
|
31
|
-
const { configPath } = parseArgs(process.argv.slice(2));
|
|
32
|
-
const absoluteConfigPath = path.resolve(rootDir, configPath);
|
|
33
|
-
|
|
34
|
-
if (!fs.existsSync(absoluteConfigPath)) {
|
|
35
|
-
console.error(`[rn-i18n] Sync configuration not found: ${configPath}`);
|
|
36
|
-
return 2;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let config: SyncConfig;
|
|
40
|
-
try {
|
|
41
|
-
config = JSON.parse(fs.readFileSync(absoluteConfigPath, 'utf8')) as SyncConfig;
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error(`[rn-i18n] Sync configuration is not valid JSON: ${String(error)}`);
|
|
44
|
-
return 2;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const { report, generatedKeysChanged } = await runSync(rootDir, config);
|
|
49
|
-
console.log(renderReport(report));
|
|
50
|
-
if (config.generateKeysFile) {
|
|
51
|
-
console.log(
|
|
52
|
-
generatedKeysChanged
|
|
53
|
-
? `Typed keys regenerated: ${config.generateKeysFile}`
|
|
54
|
-
: `Typed keys unchanged: ${config.generateKeysFile}`,
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
return report.outcome === 'failed' ? 1 : 0;
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if (error instanceof SyncConfigError) {
|
|
60
|
-
console.error(`[rn-i18n] ${error.message}`);
|
|
61
|
-
} else {
|
|
62
|
-
console.error(`[rn-i18n] Sync failed: ${String(error)}`);
|
|
63
|
-
}
|
|
64
|
-
return 2;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
main().then(
|
|
69
|
-
code => {
|
|
70
|
-
process.exitCode = code;
|
|
71
|
-
},
|
|
72
|
-
error => {
|
|
73
|
-
console.error(`[rn-i18n] Unexpected failure: ${String(error)}`);
|
|
74
|
-
process.exitCode = 2;
|
|
75
|
-
},
|
|
76
|
-
);
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import type { RuntimeEntryConfig, TranslationTemplate } from './types.ts';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Renders the typed key contract: the union of scanned template keys and
|
|
7
|
-
* declared runtime-only keys (FR-041). Apps bind lookups to the generated
|
|
8
|
-
* type so `tsc --noEmit` reports invalid or stale key usage before release.
|
|
9
|
-
*/
|
|
10
|
-
export function renderKeysModule(
|
|
11
|
-
template: TranslationTemplate,
|
|
12
|
-
runtimeEntries: readonly RuntimeEntryConfig[],
|
|
13
|
-
typeName = 'AppI18nKey',
|
|
14
|
-
): string {
|
|
15
|
-
const templateKeys = template.keys.map(key => key.key).sort((a, b) => a.localeCompare(b, 'en'));
|
|
16
|
-
const runtimeKeys = runtimeEntries
|
|
17
|
-
.map(entry => entry.key)
|
|
18
|
-
.sort((a, b) => a.localeCompare(b, 'en'));
|
|
19
|
-
|
|
20
|
-
const renderList = (keys: readonly string[]) =>
|
|
21
|
-
keys.length === 0 ? '[]' : `[\n${keys.map(key => `\t'${key}',`).join('\n')}\n]`;
|
|
22
|
-
|
|
23
|
-
return [
|
|
24
|
-
'// AUTO-GENERATED by @chipmobilesdk/rn-i18n tooling. Do not edit by hand.',
|
|
25
|
-
'// Regenerate with: npm run i18n:sync',
|
|
26
|
-
'',
|
|
27
|
-
`export const I18N_TEMPLATE_KEYS = ${renderList(templateKeys)} as const;`,
|
|
28
|
-
'',
|
|
29
|
-
`export const I18N_RUNTIME_ONLY_KEYS = ${renderList(runtimeKeys)} as const;`,
|
|
30
|
-
'',
|
|
31
|
-
'export const I18N_KEYS = [',
|
|
32
|
-
'\t...I18N_TEMPLATE_KEYS,',
|
|
33
|
-
'\t...I18N_RUNTIME_ONLY_KEYS,',
|
|
34
|
-
'] as const;',
|
|
35
|
-
'',
|
|
36
|
-
`export type ${typeName} = (typeof I18N_KEYS)[number];`,
|
|
37
|
-
'',
|
|
38
|
-
].join('\n');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface GenerateKeysResult {
|
|
42
|
-
filePath: string;
|
|
43
|
-
changed: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Writes the typed key artifact and reports whether it changed (staleness). */
|
|
47
|
-
export function generateKeysFile(
|
|
48
|
-
rootDir: string,
|
|
49
|
-
relativePath: string,
|
|
50
|
-
template: TranslationTemplate,
|
|
51
|
-
runtimeEntries: readonly RuntimeEntryConfig[],
|
|
52
|
-
typeName?: string,
|
|
53
|
-
): GenerateKeysResult {
|
|
54
|
-
const filePath = path.join(rootDir, relativePath);
|
|
55
|
-
const content = renderKeysModule(template, runtimeEntries, typeName);
|
|
56
|
-
const previous = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : undefined;
|
|
57
|
-
if (previous !== content) {
|
|
58
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
59
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
60
|
-
}
|
|
61
|
-
return { filePath, changed: previous !== content };
|
|
62
|
-
}
|
package/src/tooling/index.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Programmatic surface of the Node-only extraction and synchronization
|
|
3
|
-
* tooling, exposed as `@chipmobilesdk/rn-i18n/tooling`. This entrypoint must
|
|
4
|
-
* never be imported from React Native code; the runtime API lives at the
|
|
5
|
-
* package root (LIB-003).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export { globToRegExp, listSourceFiles, scanSource } from './scan.ts';
|
|
9
|
-
export { flattenTranslations, nestTranslations, syncLocaleFiles } from './sync.ts';
|
|
10
|
-
export { buildReport, renderReport } from './report.ts';
|
|
11
|
-
export { generateKeysFile, renderKeysModule } from './generateKeys.ts';
|
|
12
|
-
export type { GenerateKeysResult } from './generateKeys.ts';
|
|
13
|
-
export { SyncConfigError, loadRuntimeEntries, runSync, validateSyncConfig } from './runner.ts';
|
|
14
|
-
export type { SyncRunResult } from './runner.ts';
|
|
15
|
-
export type {
|
|
16
|
-
AmbiguousUsage,
|
|
17
|
-
ExtractionReport,
|
|
18
|
-
KeyConflict,
|
|
19
|
-
LocaleSyncResult,
|
|
20
|
-
PatternConfig,
|
|
21
|
-
ReportOutcome,
|
|
22
|
-
RuntimeEntryConfig,
|
|
23
|
-
SourceLocation,
|
|
24
|
-
SyncConfig,
|
|
25
|
-
TemplateKey,
|
|
26
|
-
TranslationTemplate,
|
|
27
|
-
} from './types.ts';
|
package/src/tooling/report.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtractionReport,
|
|
3
|
-
LocaleSyncResult,
|
|
4
|
-
ReportOutcome,
|
|
5
|
-
RuntimeEntryConfig,
|
|
6
|
-
TranslationTemplate,
|
|
7
|
-
} from './types.ts';
|
|
8
|
-
|
|
9
|
-
function perLocale(
|
|
10
|
-
results: readonly LocaleSyncResult[],
|
|
11
|
-
select: (result: LocaleSyncResult) => string[],
|
|
12
|
-
): Record<string, string[]> {
|
|
13
|
-
const record: Record<string, string[]> = {};
|
|
14
|
-
for (const result of results) {
|
|
15
|
-
record[result.locale] = select(result);
|
|
16
|
-
}
|
|
17
|
-
return record;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Assembles the extraction/synchronization report distinguishing scanned
|
|
22
|
-
* template keys, runtime-only keys, stale removals, ambiguous usages, and
|
|
23
|
-
* conflicts (FR-026, FR-036).
|
|
24
|
-
*/
|
|
25
|
-
export function buildReport(
|
|
26
|
-
template: TranslationTemplate,
|
|
27
|
-
syncResults: readonly LocaleSyncResult[],
|
|
28
|
-
runtimeEntries: readonly RuntimeEntryConfig[],
|
|
29
|
-
): ExtractionReport {
|
|
30
|
-
const errors = syncResults.filter(result => result.error).map(result => result.error!);
|
|
31
|
-
|
|
32
|
-
let outcome: ReportOutcome = 'success';
|
|
33
|
-
if (
|
|
34
|
-
template.ambiguous.length > 0 ||
|
|
35
|
-
syncResults.some(result => result.addedKeys.length > 0 || result.runtimeReported.length > 0)
|
|
36
|
-
) {
|
|
37
|
-
outcome = 'completedWithWarnings';
|
|
38
|
-
}
|
|
39
|
-
if (errors.length > 0 || template.conflicts.length > 0) {
|
|
40
|
-
outcome = 'failed';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
outcome,
|
|
45
|
-
discovered: template.keys.map(key => ({
|
|
46
|
-
key: key.key,
|
|
47
|
-
defaultText: key.defaultText,
|
|
48
|
-
locations: key.locations.map(location => `${location.file}:${location.line}`),
|
|
49
|
-
})),
|
|
50
|
-
createdLocaleFiles: syncResults
|
|
51
|
-
.filter(result => result.created && !result.error)
|
|
52
|
-
.map(result => ({ locale: result.locale, filePath: result.filePath })),
|
|
53
|
-
addedKeys: perLocale(syncResults, result => result.addedKeys),
|
|
54
|
-
preservedKeys: perLocale(syncResults, result => result.preservedKeys),
|
|
55
|
-
removedStaleKeys: perLocale(syncResults, result => result.removedKeys),
|
|
56
|
-
missingPerLocale: perLocale(syncResults, result =>
|
|
57
|
-
[...result.addedKeys, ...result.runtimeAdded, ...result.runtimeReported].sort((a, b) =>
|
|
58
|
-
a.localeCompare(b, 'en'),
|
|
59
|
-
),
|
|
60
|
-
),
|
|
61
|
-
runtimeOnly: {
|
|
62
|
-
declared: runtimeEntries.map(entry => entry.key),
|
|
63
|
-
preserved: perLocale(syncResults, result => result.runtimePreserved),
|
|
64
|
-
added: perLocale(syncResults, result => result.runtimeAdded),
|
|
65
|
-
reported: perLocale(syncResults, result => result.runtimeReported),
|
|
66
|
-
},
|
|
67
|
-
ambiguous: template.ambiguous,
|
|
68
|
-
conflicts: template.conflicts,
|
|
69
|
-
errors,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Renders the report as readable text for CLI output. */
|
|
74
|
-
export function renderReport(report: ExtractionReport): string {
|
|
75
|
-
const lines: string[] = [];
|
|
76
|
-
lines.push(`i18n sync outcome: ${report.outcome}`);
|
|
77
|
-
lines.push(`Discovered keys: ${report.discovered.length}`);
|
|
78
|
-
for (const created of report.createdLocaleFiles) {
|
|
79
|
-
lines.push(`Created locale file: ${created.locale} -> ${created.filePath}`);
|
|
80
|
-
}
|
|
81
|
-
for (const [locale, keys] of Object.entries(report.addedKeys)) {
|
|
82
|
-
if (keys.length > 0) {
|
|
83
|
-
lines.push(`Added [${locale}]: ${keys.join(', ')}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
for (const [locale, keys] of Object.entries(report.removedStaleKeys)) {
|
|
87
|
-
if (keys.length > 0) {
|
|
88
|
-
lines.push(`Removed stale [${locale}]: ${keys.join(', ')}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
for (const [locale, keys] of Object.entries(report.runtimeOnly.reported)) {
|
|
92
|
-
if (keys.length > 0) {
|
|
93
|
-
lines.push(`Runtime-only missing [${locale}]: ${keys.join(', ')}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
for (const usage of report.ambiguous) {
|
|
97
|
-
lines.push(`Ambiguous usage at ${usage.file}:${usage.line}: ${usage.snippet}`);
|
|
98
|
-
}
|
|
99
|
-
for (const conflict of report.conflicts) {
|
|
100
|
-
const sources = conflict.values
|
|
101
|
-
.map(value => `"${value.text}" (${value.location.file}:${value.location.line})`)
|
|
102
|
-
.join(' vs ');
|
|
103
|
-
lines.push(`Conflict for key "${conflict.key}": ${sources}`);
|
|
104
|
-
}
|
|
105
|
-
for (const error of report.errors) {
|
|
106
|
-
lines.push(`Error: ${error}`);
|
|
107
|
-
}
|
|
108
|
-
return lines.join('\n');
|
|
109
|
-
}
|