@chipmobilesdk/rn-i18n 0.1.2 → 0.2.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 +168 -57
- package/README.md +277 -218
- package/package.json +49 -49
- package/skills/sdk-rn-i18n/SKILL.md +65 -62
- package/src/config/catalog.ts +25 -0
- package/src/config/diagnostics.ts +68 -0
- package/src/config/errors.ts +28 -0
- package/src/config/types.ts +150 -152
- package/src/config/validateConfig.ts +161 -174
- package/src/format/currency.ts +34 -27
- package/src/format/date.ts +32 -26
- package/src/format/invalid.ts +22 -16
- package/src/format/number.ts +30 -25
- package/src/format/units.ts +139 -151
- package/src/i18n.ts +231 -222
- package/src/index.ts +58 -50
- package/src/locale/activeLocale.ts +42 -45
- package/src/locale/deviceLocale.ts +50 -54
- package/src/locale/keyValueStorage.ts +27 -0
- package/src/locale/persistence.ts +59 -37
- package/src/react/I18nProvider.tsx +31 -35
- package/src/react/hooks.ts +66 -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 +161 -146
- package/src/translation/runtimeKeys.ts +27 -27
package/src/tooling/sync.ts
CHANGED
|
@@ -1,198 +1,193 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import type {
|
|
4
|
-
LocaleSyncResult,
|
|
5
|
-
RuntimeEntryConfig,
|
|
6
|
-
SyncConfig,
|
|
7
|
-
TranslationTemplate,
|
|
8
|
-
} from './types.ts';
|
|
9
|
-
|
|
10
|
-
type JsonValue = string | { [key: string]: JsonValue };
|
|
11
|
-
|
|
12
|
-
const PLURAL_CATEGORIES = new Set(['zero', 'one', 'two', 'few', 'many', 'other']);
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Mirrors the runtime plural-group leaf rule (translation/plurals.ts) so a
|
|
16
|
-
* plural variant object is synchronized as one key, not per category.
|
|
17
|
-
*/
|
|
18
|
-
function isPluralGroupValue(value: JsonValue): boolean {
|
|
19
|
-
if (typeof value !== 'object' || value === null) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
const keys = Object.keys(value);
|
|
23
|
-
return (
|
|
24
|
-
keys.length > 0 &&
|
|
25
|
-
'other' in value &&
|
|
26
|
-
keys.every(
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
next.set(key,
|
|
173
|
-
result.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
result.removedKeys.sort((a, b) => a.localeCompare(b, 'en'));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return results;
|
|
198
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type {
|
|
4
|
+
LocaleSyncResult,
|
|
5
|
+
RuntimeEntryConfig,
|
|
6
|
+
SyncConfig,
|
|
7
|
+
TranslationTemplate,
|
|
8
|
+
} from './types.ts';
|
|
9
|
+
|
|
10
|
+
type JsonValue = string | { [key: string]: JsonValue };
|
|
11
|
+
|
|
12
|
+
const PLURAL_CATEGORIES = new Set(['zero', 'one', 'two', 'few', 'many', 'other']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mirrors the runtime plural-group leaf rule (translation/plurals.ts) so a
|
|
16
|
+
* plural variant object is synchronized as one key, not per category.
|
|
17
|
+
*/
|
|
18
|
+
function isPluralGroupValue(value: JsonValue): boolean {
|
|
19
|
+
if (typeof value !== 'object' || value === null) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const keys = Object.keys(value);
|
|
23
|
+
return (
|
|
24
|
+
keys.length > 0 &&
|
|
25
|
+
'other' in value &&
|
|
26
|
+
keys.every(key => PLURAL_CATEGORIES.has(key) && typeof value[key] === 'string')
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Flattens nested locale JSON into dot-path keys with leaf values. */
|
|
31
|
+
export function flattenTranslations(
|
|
32
|
+
node: JsonValue,
|
|
33
|
+
prefix = '',
|
|
34
|
+
into: Map<string, JsonValue> = new Map(),
|
|
35
|
+
): Map<string, JsonValue> {
|
|
36
|
+
if (typeof node === 'string' || isPluralGroupValue(node)) {
|
|
37
|
+
into.set(prefix, node);
|
|
38
|
+
return into;
|
|
39
|
+
}
|
|
40
|
+
for (const [key, value] of Object.entries(node)) {
|
|
41
|
+
flattenTranslations(value, prefix ? `${prefix}.${key}` : key, into);
|
|
42
|
+
}
|
|
43
|
+
return into;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sortValue(value: JsonValue): JsonValue {
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
const sorted: { [key: string]: JsonValue } = {};
|
|
51
|
+
for (const key of Object.keys(value).sort((a, b) => a.localeCompare(b, 'en'))) {
|
|
52
|
+
sorted[key] = sortValue(value[key]);
|
|
53
|
+
}
|
|
54
|
+
return sorted;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Rebuilds the nested JSON object with deterministically sorted keys. */
|
|
58
|
+
export function nestTranslations(flat: Map<string, JsonValue>): JsonValue {
|
|
59
|
+
const root: { [key: string]: JsonValue } = {};
|
|
60
|
+
const sortedKeys = [...flat.keys()].sort((a, b) => a.localeCompare(b, 'en'));
|
|
61
|
+
for (const flatKey of sortedKeys) {
|
|
62
|
+
const segments = flatKey.split('.');
|
|
63
|
+
let node = root;
|
|
64
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
65
|
+
const segment = segments[index];
|
|
66
|
+
const next = node[segment];
|
|
67
|
+
if (typeof next !== 'object' || next === null || isPluralGroupValue(next)) {
|
|
68
|
+
node[segment] = {};
|
|
69
|
+
}
|
|
70
|
+
node = node[segment] as { [key: string]: JsonValue };
|
|
71
|
+
}
|
|
72
|
+
node[segments[segments.length - 1]] = sortValue(flat.get(flatKey)!);
|
|
73
|
+
}
|
|
74
|
+
return root;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function placeholderValue(
|
|
78
|
+
config: SyncConfig,
|
|
79
|
+
locale: string,
|
|
80
|
+
key: string,
|
|
81
|
+
defaultText: string | undefined,
|
|
82
|
+
): string {
|
|
83
|
+
const placeholder = config.placeholder ?? 'template';
|
|
84
|
+
if (placeholder === 'template') {
|
|
85
|
+
return defaultText ?? key;
|
|
86
|
+
}
|
|
87
|
+
if (typeof placeholder === 'string') {
|
|
88
|
+
return placeholder;
|
|
89
|
+
}
|
|
90
|
+
return placeholder[locale] ?? defaultText ?? key;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function writeLocaleFile(filePath: string, content: JsonValue): void {
|
|
94
|
+
// Atomic per-file write: a failed run never leaves a partial locale file.
|
|
95
|
+
const json = `${JSON.stringify(content, null, '\t')}\n`;
|
|
96
|
+
const temporaryPath = `${filePath}.tmp`;
|
|
97
|
+
fs.writeFileSync(temporaryPath, json, 'utf8');
|
|
98
|
+
fs.renameSync(temporaryPath, filePath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Synchronizes every configured locale JSON file against the scanned
|
|
103
|
+
* template: creates missing files, adds missing keys with the configured
|
|
104
|
+
* placeholder, preserves existing translations verbatim, removes stale keys,
|
|
105
|
+
* and protects declared runtime-only entries (FR-027…FR-030, FR-034/FR-035).
|
|
106
|
+
* Conflicted keys are never added or overwritten while the conflict is open.
|
|
107
|
+
*/
|
|
108
|
+
export function syncLocaleFiles(
|
|
109
|
+
rootDir: string,
|
|
110
|
+
config: SyncConfig,
|
|
111
|
+
template: TranslationTemplate,
|
|
112
|
+
runtimeEntries: readonly RuntimeEntryConfig[],
|
|
113
|
+
): LocaleSyncResult[] {
|
|
114
|
+
const templateByKey = new Map(template.keys.map(key => [key.key, key]));
|
|
115
|
+
const conflictedKeys = new Set(template.conflicts.map(conflict => conflict.key));
|
|
116
|
+
const runtimeByKey = new Map(runtimeEntries.map(entry => [entry.key, entry]));
|
|
117
|
+
const results: LocaleSyncResult[] = [];
|
|
118
|
+
|
|
119
|
+
for (const locale of config.locales) {
|
|
120
|
+
const filePath = path.join(rootDir, config.localesDir, `${locale}.json`);
|
|
121
|
+
const result: LocaleSyncResult = {
|
|
122
|
+
locale,
|
|
123
|
+
filePath,
|
|
124
|
+
created: false,
|
|
125
|
+
addedKeys: [],
|
|
126
|
+
preservedKeys: [],
|
|
127
|
+
removedKeys: [],
|
|
128
|
+
runtimePreserved: [],
|
|
129
|
+
runtimeAdded: [],
|
|
130
|
+
runtimeReported: [],
|
|
131
|
+
};
|
|
132
|
+
results.push(result);
|
|
133
|
+
|
|
134
|
+
let existing = new Map<string, JsonValue>();
|
|
135
|
+
if (fs.existsSync(filePath)) {
|
|
136
|
+
try {
|
|
137
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as JsonValue;
|
|
138
|
+
existing = flattenTranslations(parsed);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
result.error = `Malformed locale file ${locale}.json: ${String(error)}`;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
result.created = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const next = new Map<string, JsonValue>();
|
|
148
|
+
|
|
149
|
+
for (const [key, templateKey] of templateByKey) {
|
|
150
|
+
const current = existing.get(key);
|
|
151
|
+
if (current !== undefined) {
|
|
152
|
+
next.set(key, current);
|
|
153
|
+
result.preservedKeys.push(key);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (conflictedKeys.has(key)) {
|
|
157
|
+
// Reported for manual review; never silently added (FR-031).
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
next.set(key, placeholderValue(config, locale, key, templateKey.defaultText));
|
|
161
|
+
result.addedKeys.push(key);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const [key, entry] of runtimeByKey) {
|
|
165
|
+
const current = existing.get(key);
|
|
166
|
+
if (current !== undefined) {
|
|
167
|
+
next.set(key, current);
|
|
168
|
+
result.runtimePreserved.push(key);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if ((entry.policy ?? 'report') === 'add') {
|
|
172
|
+
next.set(key, placeholderValue(config, locale, key, entry.defaultText));
|
|
173
|
+
result.runtimeAdded.push(key);
|
|
174
|
+
} else {
|
|
175
|
+
result.runtimeReported.push(key);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const key of existing.keys()) {
|
|
180
|
+
if (!next.has(key) && !runtimeByKey.has(key) && !conflictedKeys.has(key)) {
|
|
181
|
+
result.removedKeys.push(key);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
186
|
+
writeLocaleFile(filePath, nestTranslations(next));
|
|
187
|
+
result.addedKeys.sort((a, b) => a.localeCompare(b, 'en'));
|
|
188
|
+
result.preservedKeys.sort((a, b) => a.localeCompare(b, 'en'));
|
|
189
|
+
result.removedKeys.sort((a, b) => a.localeCompare(b, 'en'));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return results;
|
|
193
|
+
}
|
package/src/tooling/types.ts
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared contracts for the Node-only extraction and synchronization tooling.
|
|
3
|
-
* This module must stay free of React Native imports; it is consumed through
|
|
4
|
-
* the `@chipmobilesdk/rn-i18n/tooling` entrypoint and the CLI.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface PatternConfig {
|
|
8
|
-
/**
|
|
9
|
-
* Regex source with a named `key` capture group for statically resolvable
|
|
10
|
-
* keys, and an optional named `defaultText` group for template values.
|
|
11
|
-
*/
|
|
12
|
-
regex: string;
|
|
13
|
-
/** Extra regex flags; `g` is always applied. */
|
|
14
|
-
flags?: string;
|
|
15
|
-
/**
|
|
16
|
-
* Optional regex matching dynamic usages of the same call style. Matches
|
|
17
|
-
* that do not overlap a static key match are reported as ambiguous.
|
|
18
|
-
*/
|
|
19
|
-
ambiguousRegex?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface SyncConfig {
|
|
23
|
-
/** Globs relative to the project root, e.g. `src/**\/*.{ts,tsx}`. */
|
|
24
|
-
sourceGlobs: string[];
|
|
25
|
-
patterns: PatternConfig[];
|
|
26
|
-
/** Directory containing `<locale>.json` files. */
|
|
27
|
-
localesDir: string;
|
|
28
|
-
locales: string[];
|
|
29
|
-
/** Module exporting the declared runtime-only entries. */
|
|
30
|
-
runtimeEntriesFile?: string;
|
|
31
|
-
/**
|
|
32
|
-
* Value written for newly added keys: `template` uses the discovered
|
|
33
|
-
* default text (falling back to the key), a string is used verbatim, and
|
|
34
|
-
* a map selects per locale. Default: `template`.
|
|
35
|
-
*/
|
|
36
|
-
placeholder?: 'template' | string | Record<string, string>;
|
|
37
|
-
/** Optional JSON report output path. */
|
|
38
|
-
reportPath?: string;
|
|
39
|
-
/** Generated typed key artifact path, e.g. `src/i18n/keys.generated.ts`. */
|
|
40
|
-
generateKeysFile?: string;
|
|
41
|
-
/** Exported key type name. Default: `AppI18nKey`. */
|
|
42
|
-
keysTypeName?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface RuntimeEntryConfig {
|
|
46
|
-
key: string;
|
|
47
|
-
defaultText: string;
|
|
48
|
-
policy?: 'add' | 'report';
|
|
49
|
-
description?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface SourceLocation {
|
|
53
|
-
file: string;
|
|
54
|
-
line: number;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface TemplateKey {
|
|
58
|
-
key: string;
|
|
59
|
-
defaultText?: string;
|
|
60
|
-
locations: SourceLocation[];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface AmbiguousUsage {
|
|
64
|
-
file: string;
|
|
65
|
-
line: number;
|
|
66
|
-
snippet: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface KeyConflict {
|
|
70
|
-
key: string;
|
|
71
|
-
values: { text: string; location: SourceLocation }[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface TranslationTemplate {
|
|
75
|
-
/** Discovered keys sorted by key name. */
|
|
76
|
-
keys: TemplateKey[];
|
|
77
|
-
ambiguous: AmbiguousUsage[];
|
|
78
|
-
conflicts: KeyConflict[];
|
|
79
|
-
scannedFiles: number;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface LocaleSyncResult {
|
|
83
|
-
locale: string;
|
|
84
|
-
filePath: string;
|
|
85
|
-
created: boolean;
|
|
86
|
-
addedKeys: string[];
|
|
87
|
-
preservedKeys: string[];
|
|
88
|
-
removedKeys: string[];
|
|
89
|
-
runtimePreserved: string[];
|
|
90
|
-
runtimeAdded: string[];
|
|
91
|
-
runtimeReported: string[];
|
|
92
|
-
/** Set when the locale file is malformed; no write happened. */
|
|
93
|
-
error?: string;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export type ReportOutcome = 'success' | 'completedWithWarnings' | 'failed';
|
|
97
|
-
|
|
98
|
-
export interface ExtractionReport {
|
|
99
|
-
outcome: ReportOutcome;
|
|
100
|
-
discovered: {
|
|
101
|
-
key: string;
|
|
102
|
-
defaultText?: string;
|
|
103
|
-
locations: string[];
|
|
104
|
-
}[];
|
|
105
|
-
createdLocaleFiles: { locale: string; filePath: string }[];
|
|
106
|
-
addedKeys: Record<string, string[]>;
|
|
107
|
-
preservedKeys: Record<string, string[]>;
|
|
108
|
-
removedStaleKeys: Record<string, string[]>;
|
|
109
|
-
missingPerLocale: Record<string, string[]>;
|
|
110
|
-
runtimeOnly: {
|
|
111
|
-
declared: string[];
|
|
112
|
-
preserved: Record<string, string[]>;
|
|
113
|
-
added: Record<string, string[]>;
|
|
114
|
-
reported: Record<string, string[]>;
|
|
115
|
-
};
|
|
116
|
-
ambiguous: AmbiguousUsage[];
|
|
117
|
-
conflicts: KeyConflict[];
|
|
118
|
-
errors: string[];
|
|
119
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared contracts for the Node-only extraction and synchronization tooling.
|
|
3
|
+
* This module must stay free of React Native imports; it is consumed through
|
|
4
|
+
* the `@chipmobilesdk/rn-i18n/tooling` entrypoint and the CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface PatternConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Regex source with a named `key` capture group for statically resolvable
|
|
10
|
+
* keys, and an optional named `defaultText` group for template values.
|
|
11
|
+
*/
|
|
12
|
+
regex: string;
|
|
13
|
+
/** Extra regex flags; `g` is always applied. */
|
|
14
|
+
flags?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional regex matching dynamic usages of the same call style. Matches
|
|
17
|
+
* that do not overlap a static key match are reported as ambiguous.
|
|
18
|
+
*/
|
|
19
|
+
ambiguousRegex?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SyncConfig {
|
|
23
|
+
/** Globs relative to the project root, e.g. `src/**\/*.{ts,tsx}`. */
|
|
24
|
+
sourceGlobs: string[];
|
|
25
|
+
patterns: PatternConfig[];
|
|
26
|
+
/** Directory containing `<locale>.json` files. */
|
|
27
|
+
localesDir: string;
|
|
28
|
+
locales: string[];
|
|
29
|
+
/** Module exporting the declared runtime-only entries. */
|
|
30
|
+
runtimeEntriesFile?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Value written for newly added keys: `template` uses the discovered
|
|
33
|
+
* default text (falling back to the key), a string is used verbatim, and
|
|
34
|
+
* a map selects per locale. Default: `template`.
|
|
35
|
+
*/
|
|
36
|
+
placeholder?: 'template' | string | Record<string, string>;
|
|
37
|
+
/** Optional JSON report output path. */
|
|
38
|
+
reportPath?: string;
|
|
39
|
+
/** Generated typed key artifact path, e.g. `src/i18n/keys.generated.ts`. */
|
|
40
|
+
generateKeysFile?: string;
|
|
41
|
+
/** Exported key type name. Default: `AppI18nKey`. */
|
|
42
|
+
keysTypeName?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RuntimeEntryConfig {
|
|
46
|
+
key: string;
|
|
47
|
+
defaultText: string;
|
|
48
|
+
policy?: 'add' | 'report';
|
|
49
|
+
description?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SourceLocation {
|
|
53
|
+
file: string;
|
|
54
|
+
line: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TemplateKey {
|
|
58
|
+
key: string;
|
|
59
|
+
defaultText?: string;
|
|
60
|
+
locations: SourceLocation[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AmbiguousUsage {
|
|
64
|
+
file: string;
|
|
65
|
+
line: number;
|
|
66
|
+
snippet: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface KeyConflict {
|
|
70
|
+
key: string;
|
|
71
|
+
values: { text: string; location: SourceLocation }[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface TranslationTemplate {
|
|
75
|
+
/** Discovered keys sorted by key name. */
|
|
76
|
+
keys: TemplateKey[];
|
|
77
|
+
ambiguous: AmbiguousUsage[];
|
|
78
|
+
conflicts: KeyConflict[];
|
|
79
|
+
scannedFiles: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface LocaleSyncResult {
|
|
83
|
+
locale: string;
|
|
84
|
+
filePath: string;
|
|
85
|
+
created: boolean;
|
|
86
|
+
addedKeys: string[];
|
|
87
|
+
preservedKeys: string[];
|
|
88
|
+
removedKeys: string[];
|
|
89
|
+
runtimePreserved: string[];
|
|
90
|
+
runtimeAdded: string[];
|
|
91
|
+
runtimeReported: string[];
|
|
92
|
+
/** Set when the locale file is malformed; no write happened. */
|
|
93
|
+
error?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type ReportOutcome = 'success' | 'completedWithWarnings' | 'failed';
|
|
97
|
+
|
|
98
|
+
export interface ExtractionReport {
|
|
99
|
+
outcome: ReportOutcome;
|
|
100
|
+
discovered: {
|
|
101
|
+
key: string;
|
|
102
|
+
defaultText?: string;
|
|
103
|
+
locations: string[];
|
|
104
|
+
}[];
|
|
105
|
+
createdLocaleFiles: { locale: string; filePath: string }[];
|
|
106
|
+
addedKeys: Record<string, string[]>;
|
|
107
|
+
preservedKeys: Record<string, string[]>;
|
|
108
|
+
removedStaleKeys: Record<string, string[]>;
|
|
109
|
+
missingPerLocale: Record<string, string[]>;
|
|
110
|
+
runtimeOnly: {
|
|
111
|
+
declared: string[];
|
|
112
|
+
preserved: Record<string, string[]>;
|
|
113
|
+
added: Record<string, string[]>;
|
|
114
|
+
reported: Record<string, string[]>;
|
|
115
|
+
};
|
|
116
|
+
ambiguous: AmbiguousUsage[];
|
|
117
|
+
conflicts: KeyConflict[];
|
|
118
|
+
errors: string[];
|
|
119
|
+
}
|