@chipmobilesdk/rn-i18n 0.1.3 → 0.3.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 +114 -0
- package/README.md +86 -7
- package/package.json +5 -2
- package/skills/sdk-rn-i18n/SKILL.md +32 -1
- package/src/config/catalog.ts +25 -0
- package/src/config/diagnostics.ts +70 -0
- package/src/config/errors.ts +28 -0
- package/src/config/types.ts +17 -11
- package/src/config/validateConfig.ts +5 -0
- package/src/format/currency.ts +17 -5
- package/src/format/date.ts +17 -5
- package/src/format/invalid.ts +11 -2
- package/src/format/number.ts +8 -1
- package/src/format/units.ts +4 -6
- package/src/i18n.ts +158 -39
- package/src/index.ts +8 -0
- package/src/locale/activeLocale.ts +29 -23
- package/src/locale/keyValueStorage.ts +27 -0
- package/src/locale/persistence.ts +50 -3
- package/src/react/hooks.ts +18 -9
- package/src/tooling/generateKeys.ts +4 -1
- package/src/translation/plurals.ts +74 -5
- package/src/translation/resolver.ts +45 -24
- package/src/translation/runtimeKeys.ts +28 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { I18nDiagnostic, I18nDiagnosticHandler } from '../config/diagnostics';
|
|
1
2
|
import type {
|
|
2
3
|
LocaleConfig,
|
|
3
4
|
MissingTranslationPolicy,
|
|
@@ -18,14 +19,30 @@ export interface ResolverContext {
|
|
|
18
19
|
missingTranslation: MissingTranslationPolicy;
|
|
19
20
|
/** Dedupe set so each failed lookup path warns exactly once. */
|
|
20
21
|
warned: Set<string>;
|
|
22
|
+
report: I18nDiagnosticHandler;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
/**
|
|
26
|
+
* How many lookup paths are remembered. An app that misses many distinct keys
|
|
27
|
+
* forgets the oldest first, so memory stays bounded (I18N-11).
|
|
28
|
+
*/
|
|
29
|
+
const WARNED_CAPACITY = 500;
|
|
30
|
+
|
|
31
|
+
export function warnOnce(
|
|
32
|
+
context: Pick<ResolverContext, 'warned' | 'report'>,
|
|
33
|
+
dedupeKey: string,
|
|
34
|
+
diagnostic: I18nDiagnostic,
|
|
35
|
+
): void {
|
|
36
|
+
const { warned } = context;
|
|
24
37
|
if (warned.has(dedupeKey)) {
|
|
25
38
|
return;
|
|
26
39
|
}
|
|
27
40
|
warned.add(dedupeKey);
|
|
28
|
-
|
|
41
|
+
if (warned.size > WARNED_CAPACITY) {
|
|
42
|
+
const oldest = warned.values().next().value;
|
|
43
|
+
if (oldest !== undefined) warned.delete(oldest);
|
|
44
|
+
}
|
|
45
|
+
context.report(diagnostic);
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
/** Walks a dot-path key through a nested translation namespace. */
|
|
@@ -54,7 +71,7 @@ function pickValue(
|
|
|
54
71
|
node: TranslationNode,
|
|
55
72
|
locale: LocaleConfig,
|
|
56
73
|
options: TranslateOptions | undefined,
|
|
57
|
-
|
|
74
|
+
warnings: Pick<ResolverContext, 'warned' | 'report'>,
|
|
58
75
|
key: string,
|
|
59
76
|
): string | undefined {
|
|
60
77
|
if (typeof node === 'string') {
|
|
@@ -67,11 +84,12 @@ function pickValue(
|
|
|
67
84
|
}
|
|
68
85
|
return selectPluralVariant(node, resolvePluralRule(locale)(count));
|
|
69
86
|
}
|
|
70
|
-
warnOnce(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
warnOnce(warnings, `invalid-value|${locale.code}|${key}`, {
|
|
88
|
+
code: 'invalid_value',
|
|
89
|
+
key,
|
|
90
|
+
locale: locale.code,
|
|
91
|
+
message: `Translation value for "${key}" in locale "${locale.code}" is not text or a plural group; treating it as missing.`,
|
|
92
|
+
});
|
|
75
93
|
return undefined;
|
|
76
94
|
}
|
|
77
95
|
|
|
@@ -96,11 +114,12 @@ export function translate(
|
|
|
96
114
|
|
|
97
115
|
const interpolateWith = (text: string, sourceLocale: string) =>
|
|
98
116
|
interpolate(text, mergedVars, name =>
|
|
99
|
-
warnOnce(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
warnOnce(context, `missing-var|${sourceLocale}|${key}|${name}`, {
|
|
118
|
+
code: 'missing_variable',
|
|
119
|
+
key,
|
|
120
|
+
locale: sourceLocale,
|
|
121
|
+
message: `Missing interpolation variable "${name}" for key "${key}" in locale "${sourceLocale}".`,
|
|
122
|
+
}),
|
|
104
123
|
);
|
|
105
124
|
|
|
106
125
|
for (const code of chain) {
|
|
@@ -112,16 +131,17 @@ export function translate(
|
|
|
112
131
|
if (node === undefined) {
|
|
113
132
|
continue;
|
|
114
133
|
}
|
|
115
|
-
const value = pickValue(node, locale, options, context
|
|
134
|
+
const value = pickValue(node, locale, options, context, key);
|
|
116
135
|
if (value === undefined) {
|
|
117
136
|
continue;
|
|
118
137
|
}
|
|
119
138
|
if (code !== activeCode) {
|
|
120
|
-
warnOnce(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
warnOnce(context, `fallback|${activeCode}|${key}`, {
|
|
140
|
+
code: 'fallback_used',
|
|
141
|
+
key,
|
|
142
|
+
locale: activeCode,
|
|
143
|
+
message: `Key "${key}" is missing in locale "${activeCode}"; served from fallback locale "${code}".`,
|
|
144
|
+
});
|
|
125
145
|
}
|
|
126
146
|
return interpolateWith(value, code);
|
|
127
147
|
}
|
|
@@ -131,10 +151,11 @@ export function translate(
|
|
|
131
151
|
return interpolateWith(runtimeEntry.defaultText, activeCode);
|
|
132
152
|
}
|
|
133
153
|
|
|
134
|
-
warnOnce(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
154
|
+
warnOnce(context, `missing|${activeCode}|${key}`, {
|
|
155
|
+
code: 'missing_translation',
|
|
156
|
+
key,
|
|
157
|
+
locale: activeCode,
|
|
158
|
+
message: `Missing translation for key "${key}" in locale "${activeCode}" and its entire fallback chain.`,
|
|
159
|
+
});
|
|
139
160
|
return context.missingTranslation.mode === 'placeholder' ? context.missingTranslation.text : key;
|
|
140
161
|
}
|
|
@@ -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
|
}
|