@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,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
+ }
@@ -1,126 +1,111 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { pathToFileURL } from 'node:url';
4
- import { scanSource } from './scan.ts';
5
- import { syncLocaleFiles } from './sync.ts';
6
- import { buildReport } from './report.ts';
7
- import { generateKeysFile } from './generateKeys.ts';
8
- import type {
9
- ExtractionReport,
10
- RuntimeEntryConfig,
11
- SyncConfig,
12
- } from './types.ts';
13
-
14
- export class SyncConfigError extends Error {
15
- constructor(message: string) {
16
- super(message);
17
- this.name = 'SyncConfigError';
18
- }
19
- }
20
-
21
- export function validateSyncConfig(config: SyncConfig): void {
22
- const issues: string[] = [];
23
- if (!Array.isArray(config.sourceGlobs) || config.sourceGlobs.length === 0) {
24
- issues.push('`sourceGlobs` must list at least one glob.');
25
- }
26
- if (!Array.isArray(config.patterns) || config.patterns.length === 0) {
27
- issues.push('`patterns` must list at least one regex pattern.');
28
- } else {
29
- for (const pattern of config.patterns) {
30
- if (!pattern.regex?.includes('(?<key>')) {
31
- issues.push(
32
- `Pattern "${pattern.regex}" must contain a named (?<key>...) capture group.`,
33
- );
34
- }
35
- }
36
- }
37
- if (!config.localesDir) {
38
- issues.push('`localesDir` is required.');
39
- }
40
- if (!Array.isArray(config.locales) || config.locales.length === 0) {
41
- issues.push('`locales` must list every supported locale.');
42
- }
43
- if (issues.length > 0) {
44
- throw new SyncConfigError(
45
- `Invalid i18n sync configuration:\n- ${issues.join('\n- ')}`,
46
- );
47
- }
48
- }
49
-
50
- /**
51
- * Loads declared runtime-only entries from the configured module. Accepts a
52
- * default export or a named `runtimeEntries` export. The module must use
53
- * type-only imports so it can run under Node's TypeScript type stripping.
54
- */
55
- export async function loadRuntimeEntries(
56
- rootDir: string,
57
- runtimeEntriesFile: string | undefined,
58
- ): Promise<RuntimeEntryConfig[]> {
59
- if (!runtimeEntriesFile) {
60
- return [];
61
- }
62
- const absolutePath = path.resolve(rootDir, runtimeEntriesFile);
63
- if (!fs.existsSync(absolutePath)) {
64
- throw new SyncConfigError(
65
- `Runtime entries file not found: ${runtimeEntriesFile}`,
66
- );
67
- }
68
- const module = (await import(pathToFileURL(absolutePath).href)) as {
69
- default?: RuntimeEntryConfig[];
70
- runtimeEntries?: RuntimeEntryConfig[];
71
- };
72
- const entries = module.default ?? module.runtimeEntries;
73
- if (!Array.isArray(entries)) {
74
- throw new SyncConfigError(
75
- `Runtime entries file must export an entry array (default or \`runtimeEntries\`): ${runtimeEntriesFile}`,
76
- );
77
- }
78
- return entries;
79
- }
80
-
81
- export interface SyncRunResult {
82
- report: ExtractionReport;
83
- generatedKeysChanged: boolean;
84
- }
85
-
86
- /**
87
- * Full pipeline: scan the source tree, synchronize every locale JSON file,
88
- * regenerate the typed key artifact, and assemble the report.
89
- */
90
- export async function runSync(
91
- rootDir: string,
92
- config: SyncConfig,
93
- runtimeEntriesOverride?: readonly RuntimeEntryConfig[],
94
- ): Promise<SyncRunResult> {
95
- validateSyncConfig(config);
96
- const runtimeEntries =
97
- runtimeEntriesOverride ??
98
- (await loadRuntimeEntries(rootDir, config.runtimeEntriesFile));
99
-
100
- const template = scanSource(rootDir, config.sourceGlobs, config.patterns);
101
- const syncResults = syncLocaleFiles(rootDir, config, template, runtimeEntries);
102
- const report = buildReport(template, syncResults, runtimeEntries);
103
-
104
- let generatedKeysChanged = false;
105
- if (config.generateKeysFile) {
106
- generatedKeysChanged = generateKeysFile(
107
- rootDir,
108
- config.generateKeysFile,
109
- template,
110
- runtimeEntries,
111
- config.keysTypeName,
112
- ).changed;
113
- }
114
-
115
- if (config.reportPath) {
116
- const reportPath = path.resolve(rootDir, config.reportPath);
117
- fs.mkdirSync(path.dirname(reportPath), { recursive: true });
118
- fs.writeFileSync(
119
- reportPath,
120
- `${JSON.stringify(report, null, '\t')}\n`,
121
- 'utf8',
122
- );
123
- }
124
-
125
- return { report, generatedKeysChanged };
126
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { scanSource } from './scan.ts';
5
+ import { syncLocaleFiles } from './sync.ts';
6
+ import { buildReport } from './report.ts';
7
+ import { generateKeysFile } from './generateKeys.ts';
8
+ import type { ExtractionReport, RuntimeEntryConfig, SyncConfig } from './types.ts';
9
+
10
+ export class SyncConfigError extends Error {
11
+ constructor(message: string) {
12
+ super(message);
13
+ this.name = 'SyncConfigError';
14
+ }
15
+ }
16
+
17
+ export function validateSyncConfig(config: SyncConfig): void {
18
+ const issues: string[] = [];
19
+ if (!Array.isArray(config.sourceGlobs) || config.sourceGlobs.length === 0) {
20
+ issues.push('`sourceGlobs` must list at least one glob.');
21
+ }
22
+ if (!Array.isArray(config.patterns) || config.patterns.length === 0) {
23
+ issues.push('`patterns` must list at least one regex pattern.');
24
+ } else {
25
+ for (const pattern of config.patterns) {
26
+ if (!pattern.regex?.includes('(?<key>')) {
27
+ issues.push(`Pattern "${pattern.regex}" must contain a named (?<key>...) capture group.`);
28
+ }
29
+ }
30
+ }
31
+ if (!config.localesDir) {
32
+ issues.push('`localesDir` is required.');
33
+ }
34
+ if (!Array.isArray(config.locales) || config.locales.length === 0) {
35
+ issues.push('`locales` must list every supported locale.');
36
+ }
37
+ if (issues.length > 0) {
38
+ throw new SyncConfigError(`Invalid i18n sync configuration:\n- ${issues.join('\n- ')}`);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Loads declared runtime-only entries from the configured module. Accepts a
44
+ * default export or a named `runtimeEntries` export. The module must use
45
+ * type-only imports so it can run under Node's TypeScript type stripping.
46
+ */
47
+ export async function loadRuntimeEntries(
48
+ rootDir: string,
49
+ runtimeEntriesFile: string | undefined,
50
+ ): Promise<RuntimeEntryConfig[]> {
51
+ if (!runtimeEntriesFile) {
52
+ return [];
53
+ }
54
+ const absolutePath = path.resolve(rootDir, runtimeEntriesFile);
55
+ if (!fs.existsSync(absolutePath)) {
56
+ throw new SyncConfigError(`Runtime entries file not found: ${runtimeEntriesFile}`);
57
+ }
58
+ const module = (await import(pathToFileURL(absolutePath).href)) as {
59
+ default?: RuntimeEntryConfig[];
60
+ runtimeEntries?: RuntimeEntryConfig[];
61
+ };
62
+ const entries = module.default ?? module.runtimeEntries;
63
+ if (!Array.isArray(entries)) {
64
+ throw new SyncConfigError(
65
+ `Runtime entries file must export an entry array (default or \`runtimeEntries\`): ${runtimeEntriesFile}`,
66
+ );
67
+ }
68
+ return entries;
69
+ }
70
+
71
+ export interface SyncRunResult {
72
+ report: ExtractionReport;
73
+ generatedKeysChanged: boolean;
74
+ }
75
+
76
+ /**
77
+ * Full pipeline: scan the source tree, synchronize every locale JSON file,
78
+ * regenerate the typed key artifact, and assemble the report.
79
+ */
80
+ export async function runSync(
81
+ rootDir: string,
82
+ config: SyncConfig,
83
+ runtimeEntriesOverride?: readonly RuntimeEntryConfig[],
84
+ ): Promise<SyncRunResult> {
85
+ validateSyncConfig(config);
86
+ const runtimeEntries =
87
+ runtimeEntriesOverride ?? (await loadRuntimeEntries(rootDir, config.runtimeEntriesFile));
88
+
89
+ const template = scanSource(rootDir, config.sourceGlobs, config.patterns);
90
+ const syncResults = syncLocaleFiles(rootDir, config, template, runtimeEntries);
91
+ const report = buildReport(template, syncResults, runtimeEntries);
92
+
93
+ let generatedKeysChanged = false;
94
+ if (config.generateKeysFile) {
95
+ generatedKeysChanged = generateKeysFile(
96
+ rootDir,
97
+ config.generateKeysFile,
98
+ template,
99
+ runtimeEntries,
100
+ config.keysTypeName,
101
+ ).changed;
102
+ }
103
+
104
+ if (config.reportPath) {
105
+ const reportPath = path.resolve(rootDir, config.reportPath);
106
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true });
107
+ fs.writeFileSync(reportPath, `${JSON.stringify(report, null, '\t')}\n`, 'utf8');
108
+ }
109
+
110
+ return { report, generatedKeysChanged };
111
+ }