@chipmobilesdk/rn-i18n 0.1.1 → 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.
@@ -1,23 +1,23 @@
1
- import type { TranslateVars } from '../config/types';
2
-
3
- const VARIABLE_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
4
-
5
- /**
6
- * Replaces `{{name}}` placeholders with named values. Missing variables keep
7
- * their placeholder text so the result stays deterministic, and each missing
8
- * name is reported through `onMissingVariable`.
9
- */
10
- export function interpolate(
11
- text: string,
12
- vars?: TranslateVars,
13
- onMissingVariable?: (name: string) => void,
14
- ): string {
15
- return text.replace(VARIABLE_PATTERN, (placeholder, name: string) => {
16
- const value = vars?.[name];
17
- if (value === undefined || value === null) {
18
- onMissingVariable?.(name);
19
- return placeholder;
20
- }
21
- return String(value);
22
- });
23
- }
1
+ import type { TranslateVars } from '../config/types';
2
+
3
+ const VARIABLE_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
4
+
5
+ /**
6
+ * Replaces `{{name}}` placeholders with named values. Missing variables keep
7
+ * their placeholder text so the result stays deterministic, and each missing
8
+ * name is reported through `onMissingVariable`.
9
+ */
10
+ export function interpolate(
11
+ text: string,
12
+ vars?: TranslateVars,
13
+ onMissingVariable?: (name: string) => void,
14
+ ): string {
15
+ return text.replace(VARIABLE_PATTERN, (placeholder, name: string) => {
16
+ const value = vars?.[name];
17
+ if (value === undefined || value === null) {
18
+ onMissingVariable?.(name);
19
+ return placeholder;
20
+ }
21
+ return String(value);
22
+ });
23
+ }
@@ -1,69 +1,62 @@
1
- import type {
2
- LocaleConfig,
3
- PluralCategory,
4
- PluralRule,
5
- TranslationNode,
6
- } from '../config/types';
7
-
8
- export const PLURAL_CATEGORIES: readonly PluralCategory[] = [
9
- 'zero',
10
- 'one',
11
- 'two',
12
- 'few',
13
- 'many',
14
- 'other',
15
- ];
16
-
17
- const oneOtherRule: PluralRule = count => (count === 1 ? 'one' : 'other');
18
- const otherOnlyRule: PluralRule = () => 'other';
19
-
20
- /**
21
- * Package-owned plural rules. Intl.PluralRules is not implemented by Hermes,
22
- * so rules are resolved from configuration to keep behavior identical across
23
- * devices and test environments. Languages without a built-in rule use the
24
- * documented default (`count === 1` selects `one`, everything else `other`)
25
- * unless the locale configuration supplies a custom rule.
26
- */
27
- const BUILT_IN_RULES: Record<string, PluralRule> = {
28
- en: oneOtherRule,
29
- ja: otherOnlyRule,
30
- vi: otherOnlyRule,
31
- };
32
-
33
- export const DEFAULT_PLURAL_RULE: PluralRule = oneOtherRule;
34
-
35
- export function resolvePluralRule(locale: LocaleConfig): PluralRule {
36
- if (locale.pluralRule) {
37
- return locale.pluralRule;
38
- }
39
- const language = locale.code.split('-')[0]?.toLowerCase() ?? '';
40
- return BUILT_IN_RULES[language] ?? DEFAULT_PLURAL_RULE;
41
- }
42
-
43
- /**
44
- * A plural group is an object whose keys are all plural categories, whose
45
- * values are all strings, and which provides the required `other` variant.
46
- */
47
- export function isPluralGroup(
48
- node: TranslationNode,
49
- ): node is Record<string, string> {
50
- if (typeof node !== 'object' || node === null) {
51
- return false;
52
- }
53
- const keys = Object.keys(node);
54
- if (keys.length === 0 || !('other' in node)) {
55
- return false;
56
- }
57
- return keys.every(
58
- key =>
59
- (PLURAL_CATEGORIES as readonly string[]).includes(key) &&
60
- typeof (node as Record<string, unknown>)[key] === 'string',
61
- );
62
- }
63
-
64
- export function selectPluralVariant(
65
- group: Record<string, string>,
66
- category: PluralCategory,
67
- ): string {
68
- return group[category] ?? group.other;
69
- }
1
+ import type { LocaleConfig, PluralCategory, PluralRule, TranslationNode } from '../config/types';
2
+
3
+ export const PLURAL_CATEGORIES: readonly PluralCategory[] = [
4
+ 'zero',
5
+ 'one',
6
+ 'two',
7
+ 'few',
8
+ 'many',
9
+ 'other',
10
+ ];
11
+
12
+ const oneOtherRule: PluralRule = count => (count === 1 ? 'one' : 'other');
13
+ const otherOnlyRule: PluralRule = () => 'other';
14
+
15
+ /**
16
+ * Package-owned plural rules. Intl.PluralRules is not implemented by Hermes,
17
+ * so rules are resolved from configuration to keep behavior identical across
18
+ * devices and test environments. Languages without a built-in rule use the
19
+ * documented default (`count === 1` selects `one`, everything else `other`)
20
+ * unless the locale configuration supplies a custom rule.
21
+ */
22
+ const BUILT_IN_RULES: Record<string, PluralRule> = {
23
+ en: oneOtherRule,
24
+ ja: otherOnlyRule,
25
+ vi: otherOnlyRule,
26
+ };
27
+
28
+ export const DEFAULT_PLURAL_RULE: PluralRule = oneOtherRule;
29
+
30
+ export function resolvePluralRule(locale: LocaleConfig): PluralRule {
31
+ if (locale.pluralRule) {
32
+ return locale.pluralRule;
33
+ }
34
+ const language = locale.code.split('-')[0]?.toLowerCase() ?? '';
35
+ return BUILT_IN_RULES[language] ?? DEFAULT_PLURAL_RULE;
36
+ }
37
+
38
+ /**
39
+ * A plural group is an object whose keys are all plural categories, whose
40
+ * values are all strings, and which provides the required `other` variant.
41
+ */
42
+ export function isPluralGroup(node: TranslationNode): node is Record<string, string> {
43
+ if (typeof node !== 'object' || node === null) {
44
+ return false;
45
+ }
46
+ const keys = Object.keys(node);
47
+ if (keys.length === 0 || !('other' in node)) {
48
+ return false;
49
+ }
50
+ return keys.every(
51
+ key =>
52
+ (PLURAL_CATEGORIES as readonly string[]).includes(key) &&
53
+ typeof (node as Record<string, unknown>)[key] === 'string',
54
+ );
55
+ }
56
+
57
+ export function selectPluralVariant(
58
+ group: Record<string, string>,
59
+ category: PluralCategory,
60
+ ): string {
61
+ return group[category] ?? group.other;
62
+ }
@@ -1,146 +1,140 @@
1
- import type {
2
- LocaleConfig,
3
- MissingTranslationPolicy,
4
- TranslateOptions,
5
- TranslateVars,
6
- TranslationNamespace,
7
- TranslationNode,
8
- TranslationResources,
9
- } from '../config/types';
10
- import { interpolate } from './interpolate';
11
- import { isPluralGroup, resolvePluralRule, selectPluralVariant } from './plurals';
12
- import type { RuntimeEntryRegistry } from './runtimeKeys';
13
-
14
- export interface ResolverContext {
15
- localesByCode: ReadonlyMap<string, LocaleConfig>;
16
- resources: TranslationResources;
17
- runtimeEntries: RuntimeEntryRegistry;
18
- missingTranslation: MissingTranslationPolicy;
19
- /** Dedupe set so each failed lookup path warns exactly once. */
20
- warned: Set<string>;
21
- }
22
-
23
- export function warnOnce(
24
- warned: Set<string>,
25
- dedupeKey: string,
26
- message: string,
27
- ): void {
28
- if (warned.has(dedupeKey)) {
29
- return;
30
- }
31
- warned.add(dedupeKey);
32
- console.warn(message);
33
- }
34
-
35
- /** Walks a dot-path key through a nested translation namespace. */
36
- export function getTranslationNode(
37
- namespace: TranslationNamespace | undefined,
38
- key: string,
39
- ): TranslationNode | undefined {
40
- if (!namespace) {
41
- return undefined;
42
- }
43
- let node: TranslationNode = namespace;
44
- for (const segment of key.split('.')) {
45
- if (typeof node !== 'object' || node === null) {
46
- return undefined;
47
- }
48
- const next: TranslationNode | undefined = node[segment];
49
- if (next === undefined) {
50
- return undefined;
51
- }
52
- node = next;
53
- }
54
- return node;
55
- }
56
-
57
- function pickValue(
58
- node: TranslationNode,
59
- locale: LocaleConfig,
60
- options: TranslateOptions | undefined,
61
- warned: Set<string>,
62
- key: string,
63
- ): string | undefined {
64
- if (typeof node === 'string') {
65
- return node;
66
- }
67
- if (isPluralGroup(node)) {
68
- const count = options?.count;
69
- if (count === undefined) {
70
- return node.other;
71
- }
72
- return selectPluralVariant(node, resolvePluralRule(locale)(count));
73
- }
74
- warnOnce(
75
- warned,
76
- `invalid-value|${locale.code}|${key}`,
77
- `[rn-i18n] Translation value for "${key}" in locale "${locale.code}" is not text or a plural group; treating it as missing.`,
78
- );
79
- return undefined;
80
- }
81
-
82
- /**
83
- * Resolves a key through the active locale, its ordered fallback chain, and
84
- * declared runtime-only default text, applying plural selection and named
85
- * variable interpolation. Misses are reported with one console warning per
86
- * lookup path; a full-chain miss applies the configured missing-translation
87
- * policy (FR-008…FR-011, FR-033).
88
- */
89
- export function translate(
90
- context: ResolverContext,
91
- activeCode: string,
92
- key: string,
93
- vars?: TranslateVars,
94
- options?: TranslateOptions,
95
- ): string {
96
- const active = context.localesByCode.get(activeCode);
97
- const chain = [activeCode, ...(active?.fallbackChain ?? [])];
98
- const mergedVars: TranslateVars | undefined =
99
- options?.count === undefined ? vars : { count: options.count, ...vars };
100
-
101
- const interpolateWith = (text: string, sourceLocale: string) =>
102
- interpolate(text, mergedVars, name =>
103
- warnOnce(
104
- context.warned,
105
- `missing-var|${sourceLocale}|${key}|${name}`,
106
- `[rn-i18n] Missing interpolation variable "${name}" for key "${key}" in locale "${sourceLocale}".`,
107
- ),
108
- );
109
-
110
- for (const code of chain) {
111
- const locale = context.localesByCode.get(code);
112
- if (!locale) {
113
- continue;
114
- }
115
- const node = getTranslationNode(context.resources[code], key);
116
- if (node === undefined) {
117
- continue;
118
- }
119
- const value = pickValue(node, locale, options, context.warned, key);
120
- if (value === undefined) {
121
- continue;
122
- }
123
- if (code !== activeCode) {
124
- warnOnce(
125
- context.warned,
126
- `fallback|${activeCode}|${key}`,
127
- `[rn-i18n] Key "${key}" is missing in locale "${activeCode}"; served from fallback locale "${code}".`,
128
- );
129
- }
130
- return interpolateWith(value, code);
131
- }
132
-
133
- const runtimeEntry = context.runtimeEntries.get(key);
134
- if (runtimeEntry) {
135
- return interpolateWith(runtimeEntry.defaultText, activeCode);
136
- }
137
-
138
- warnOnce(
139
- context.warned,
140
- `missing|${activeCode}|${key}`,
141
- `[rn-i18n] Missing translation for key "${key}" in locale "${activeCode}" and its entire fallback chain.`,
142
- );
143
- return context.missingTranslation.mode === 'placeholder'
144
- ? context.missingTranslation.text
145
- : key;
146
- }
1
+ import type {
2
+ LocaleConfig,
3
+ MissingTranslationPolicy,
4
+ TranslateOptions,
5
+ TranslateVars,
6
+ TranslationNamespace,
7
+ TranslationNode,
8
+ TranslationResources,
9
+ } from '../config/types';
10
+ import { interpolate } from './interpolate';
11
+ import { isPluralGroup, resolvePluralRule, selectPluralVariant } from './plurals';
12
+ import type { RuntimeEntryRegistry } from './runtimeKeys';
13
+
14
+ export interface ResolverContext {
15
+ localesByCode: ReadonlyMap<string, LocaleConfig>;
16
+ resources: TranslationResources;
17
+ runtimeEntries: RuntimeEntryRegistry;
18
+ missingTranslation: MissingTranslationPolicy;
19
+ /** Dedupe set so each failed lookup path warns exactly once. */
20
+ warned: Set<string>;
21
+ }
22
+
23
+ export function warnOnce(warned: Set<string>, dedupeKey: string, message: string): void {
24
+ if (warned.has(dedupeKey)) {
25
+ return;
26
+ }
27
+ warned.add(dedupeKey);
28
+ console.warn(message);
29
+ }
30
+
31
+ /** Walks a dot-path key through a nested translation namespace. */
32
+ export function getTranslationNode(
33
+ namespace: TranslationNamespace | undefined,
34
+ key: string,
35
+ ): TranslationNode | undefined {
36
+ if (!namespace) {
37
+ return undefined;
38
+ }
39
+ let node: TranslationNode = namespace;
40
+ for (const segment of key.split('.')) {
41
+ if (typeof node !== 'object' || node === null) {
42
+ return undefined;
43
+ }
44
+ const next: TranslationNode | undefined = node[segment];
45
+ if (next === undefined) {
46
+ return undefined;
47
+ }
48
+ node = next;
49
+ }
50
+ return node;
51
+ }
52
+
53
+ function pickValue(
54
+ node: TranslationNode,
55
+ locale: LocaleConfig,
56
+ options: TranslateOptions | undefined,
57
+ warned: Set<string>,
58
+ key: string,
59
+ ): string | undefined {
60
+ if (typeof node === 'string') {
61
+ return node;
62
+ }
63
+ if (isPluralGroup(node)) {
64
+ const count = options?.count;
65
+ if (count === undefined) {
66
+ return node.other;
67
+ }
68
+ return selectPluralVariant(node, resolvePluralRule(locale)(count));
69
+ }
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
+ );
75
+ return undefined;
76
+ }
77
+
78
+ /**
79
+ * Resolves a key through the active locale, its ordered fallback chain, and
80
+ * declared runtime-only default text, applying plural selection and named
81
+ * variable interpolation. Misses are reported with one console warning per
82
+ * lookup path; a full-chain miss applies the configured missing-translation
83
+ * policy (FR-008…FR-011, FR-033).
84
+ */
85
+ export function translate(
86
+ context: ResolverContext,
87
+ activeCode: string,
88
+ key: string,
89
+ vars?: TranslateVars,
90
+ options?: TranslateOptions,
91
+ ): string {
92
+ const active = context.localesByCode.get(activeCode);
93
+ const chain = [activeCode, ...(active?.fallbackChain ?? [])];
94
+ const mergedVars: TranslateVars | undefined =
95
+ options?.count === undefined ? vars : { count: options.count, ...vars };
96
+
97
+ const interpolateWith = (text: string, sourceLocale: string) =>
98
+ 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
+ ),
104
+ );
105
+
106
+ for (const code of chain) {
107
+ const locale = context.localesByCode.get(code);
108
+ if (!locale) {
109
+ continue;
110
+ }
111
+ const node = getTranslationNode(context.resources[code], key);
112
+ if (node === undefined) {
113
+ continue;
114
+ }
115
+ const value = pickValue(node, locale, options, context.warned, key);
116
+ if (value === undefined) {
117
+ continue;
118
+ }
119
+ 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
+ );
125
+ }
126
+ return interpolateWith(value, code);
127
+ }
128
+
129
+ const runtimeEntry = context.runtimeEntries.get(key);
130
+ if (runtimeEntry) {
131
+ return interpolateWith(runtimeEntry.defaultText, activeCode);
132
+ }
133
+
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
+ );
139
+ return context.missingTranslation.mode === 'placeholder' ? context.missingTranslation.text : key;
140
+ }
@@ -1,27 +1,27 @@
1
- import type { RuntimeTranslationEntry } from '../config/types';
2
-
3
- /**
4
- * Registry of declared runtime-only translation entries. Entries participate
5
- * in the same lookup pipeline as scanned template keys and are protected from
6
- * removal during locale JSON synchronization.
7
- */
8
- export type RuntimeEntryRegistry = Map<string, RuntimeTranslationEntry>;
9
-
10
- export function createRuntimeRegistry(
11
- entries?: readonly RuntimeTranslationEntry[],
12
- ): RuntimeEntryRegistry {
13
- const registry: RuntimeEntryRegistry = new Map();
14
- if (entries) {
15
- registerRuntimeEntries(registry, entries);
16
- }
17
- return registry;
18
- }
19
-
20
- export function registerRuntimeEntries(
21
- registry: RuntimeEntryRegistry,
22
- entries: readonly RuntimeTranslationEntry[],
23
- ): void {
24
- for (const entry of entries) {
25
- registry.set(entry.key, entry);
26
- }
27
- }
1
+ import type { RuntimeTranslationEntry } from '../config/types';
2
+
3
+ /**
4
+ * Registry of declared runtime-only translation entries. Entries participate
5
+ * in the same lookup pipeline as scanned template keys and are protected from
6
+ * removal during locale JSON synchronization.
7
+ */
8
+ export type RuntimeEntryRegistry = Map<string, RuntimeTranslationEntry>;
9
+
10
+ export function createRuntimeRegistry(
11
+ entries?: readonly RuntimeTranslationEntry[],
12
+ ): RuntimeEntryRegistry {
13
+ const registry: RuntimeEntryRegistry = new Map();
14
+ if (entries) {
15
+ registerRuntimeEntries(registry, entries);
16
+ }
17
+ return registry;
18
+ }
19
+
20
+ export function registerRuntimeEntries(
21
+ registry: RuntimeEntryRegistry,
22
+ entries: readonly RuntimeTranslationEntry[],
23
+ ): void {
24
+ for (const entry of entries) {
25
+ registry.set(entry.key, entry);
26
+ }
27
+ }