@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
|
@@ -1,68 +1,62 @@
|
|
|
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
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
if (previous !== content) {
|
|
64
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
65
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
66
|
-
}
|
|
67
|
-
return { filePath, changed: previous !== content };
|
|
68
|
-
}
|
|
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
CHANGED
|
@@ -1,32 +1,27 @@
|
|
|
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 {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
SourceLocation,
|
|
29
|
-
SyncConfig,
|
|
30
|
-
TemplateKey,
|
|
31
|
-
TranslationTemplate,
|
|
32
|
-
} from './types.ts';
|
|
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
CHANGED
|
@@ -1,117 +1,109 @@
|
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
outcome
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
),
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
lines.push(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
.join(' vs ');
|
|
111
|
-
lines.push(`Conflict for key "${conflict.key}": ${sources}`);
|
|
112
|
-
}
|
|
113
|
-
for (const error of report.errors) {
|
|
114
|
-
lines.push(`Error: ${error}`);
|
|
115
|
-
}
|
|
116
|
-
return lines.join('\n');
|
|
117
|
-
}
|
|
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
|
+
}
|