@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.
@@ -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
- export function warnOnce(warned: Set<string>, dedupeKey: string, message: string): void {
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
- console.warn(message);
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
- warned: Set<string>,
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
- warned,
72
- `invalid-value|${locale.code}|${key}`,
73
- `[rn-i18n] Translation value for "${key}" in locale "${locale.code}" is not text or a plural group; treating it as missing.`,
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
- context.warned,
101
- `missing-var|${sourceLocale}|${key}|${name}`,
102
- `[rn-i18n] Missing interpolation variable "${name}" for key "${key}" in locale "${sourceLocale}".`,
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.warned, key);
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
- context.warned,
122
- `fallback|${activeCode}|${key}`,
123
- `[rn-i18n] Key "${key}" is missing in locale "${activeCode}"; served from fallback locale "${code}".`,
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
- context.warned,
136
- `missing|${activeCode}|${key}`,
137
- `[rn-i18n] Missing translation for key "${key}" in locale "${activeCode}" and its entire fallback chain.`,
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 { RuntimeTranslationEntry } from '../config/types';
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
  }