@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.
@@ -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
- .map(key => key.key)
17
- .sort((a, b) => a.localeCompare(b, 'en'));
18
- const runtimeKeys = runtimeEntries
19
- .map(entry => entry.key)
20
- .sort((a, b) => a.localeCompare(b, 'en'));
21
-
22
- const renderList = (keys: readonly string[]) =>
23
- keys.length === 0
24
- ? '[]'
25
- : `[\n${keys.map(key => `\t'${key}',`).join('\n')}\n]`;
26
-
27
- return [
28
- '// AUTO-GENERATED by @chipmobilesdk/rn-i18n tooling. Do not edit by hand.',
29
- '// Regenerate with: npm run i18n:sync',
30
- '',
31
- `export const I18N_TEMPLATE_KEYS = ${renderList(templateKeys)} as const;`,
32
- '',
33
- `export const I18N_RUNTIME_ONLY_KEYS = ${renderList(runtimeKeys)} as const;`,
34
- '',
35
- 'export const I18N_KEYS = [',
36
- '\t...I18N_TEMPLATE_KEYS,',
37
- '\t...I18N_RUNTIME_ONLY_KEYS,',
38
- '] as const;',
39
- '',
40
- `export type ${typeName} = (typeof I18N_KEYS)[number];`,
41
- '',
42
- ].join('\n');
43
- }
44
-
45
- export interface GenerateKeysResult {
46
- filePath: string;
47
- changed: boolean;
48
- }
49
-
50
- /** Writes the typed key artifact and reports whether it changed (staleness). */
51
- export function generateKeysFile(
52
- rootDir: string,
53
- relativePath: string,
54
- template: TranslationTemplate,
55
- runtimeEntries: readonly RuntimeEntryConfig[],
56
- typeName?: string,
57
- ): GenerateKeysResult {
58
- const filePath = path.join(rootDir, relativePath);
59
- const content = renderKeysModule(template, runtimeEntries, typeName);
60
- const previous = fs.existsSync(filePath)
61
- ? fs.readFileSync(filePath, 'utf8')
62
- : undefined;
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
+ }
@@ -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
- SyncConfigError,
15
- loadRuntimeEntries,
16
- runSync,
17
- validateSyncConfig,
18
- } from './runner.ts';
19
- export type { SyncRunResult } from './runner.ts';
20
- export type {
21
- AmbiguousUsage,
22
- ExtractionReport,
23
- KeyConflict,
24
- LocaleSyncResult,
25
- PatternConfig,
26
- ReportOutcome,
27
- RuntimeEntryConfig,
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';
@@ -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
- .filter(result => result.error)
32
- .map(result => result.error!);
33
-
34
- let outcome: ReportOutcome = 'success';
35
- if (
36
- template.ambiguous.length > 0 ||
37
- syncResults.some(
38
- result => result.addedKeys.length > 0 || result.runtimeReported.length > 0,
39
- )
40
- ) {
41
- outcome = 'completedWithWarnings';
42
- }
43
- if (errors.length > 0 || template.conflicts.length > 0) {
44
- outcome = 'failed';
45
- }
46
-
47
- return {
48
- outcome,
49
- discovered: template.keys.map(key => ({
50
- key: key.key,
51
- defaultText: key.defaultText,
52
- locations: key.locations.map(
53
- location => `${location.file}:${location.line}`,
54
- ),
55
- })),
56
- createdLocaleFiles: syncResults
57
- .filter(result => result.created && !result.error)
58
- .map(result => ({ locale: result.locale, filePath: result.filePath })),
59
- addedKeys: perLocale(syncResults, result => result.addedKeys),
60
- preservedKeys: perLocale(syncResults, result => result.preservedKeys),
61
- removedStaleKeys: perLocale(syncResults, result => result.removedKeys),
62
- missingPerLocale: perLocale(syncResults, result =>
63
- [...result.addedKeys, ...result.runtimeAdded, ...result.runtimeReported].sort(
64
- (a, b) => a.localeCompare(b, 'en'),
65
- ),
66
- ),
67
- runtimeOnly: {
68
- declared: runtimeEntries.map(entry => entry.key),
69
- preserved: perLocale(syncResults, result => result.runtimePreserved),
70
- added: perLocale(syncResults, result => result.runtimeAdded),
71
- reported: perLocale(syncResults, result => result.runtimeReported),
72
- },
73
- ambiguous: template.ambiguous,
74
- conflicts: template.conflicts,
75
- errors,
76
- };
77
- }
78
-
79
- /** Renders the report as readable text for CLI output. */
80
- export function renderReport(report: ExtractionReport): string {
81
- const lines: string[] = [];
82
- lines.push(`i18n sync outcome: ${report.outcome}`);
83
- lines.push(`Discovered keys: ${report.discovered.length}`);
84
- for (const created of report.createdLocaleFiles) {
85
- lines.push(`Created locale file: ${created.locale} -> ${created.filePath}`);
86
- }
87
- for (const [locale, keys] of Object.entries(report.addedKeys)) {
88
- if (keys.length > 0) {
89
- lines.push(`Added [${locale}]: ${keys.join(', ')}`);
90
- }
91
- }
92
- for (const [locale, keys] of Object.entries(report.removedStaleKeys)) {
93
- if (keys.length > 0) {
94
- lines.push(`Removed stale [${locale}]: ${keys.join(', ')}`);
95
- }
96
- }
97
- for (const [locale, keys] of Object.entries(report.runtimeOnly.reported)) {
98
- if (keys.length > 0) {
99
- lines.push(`Runtime-only missing [${locale}]: ${keys.join(', ')}`);
100
- }
101
- }
102
- for (const usage of report.ambiguous) {
103
- lines.push(
104
- `Ambiguous usage at ${usage.file}:${usage.line}: ${usage.snippet}`,
105
- );
106
- }
107
- for (const conflict of report.conflicts) {
108
- const sources = conflict.values
109
- .map(value => `"${value.text}" (${value.location.file}:${value.location.line})`)
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
+ }