@chipmobilesdk/rn-i18n 0.1.2 → 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,210 +1,193 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import type {
4
- AmbiguousUsage,
5
- KeyConflict,
6
- PatternConfig,
7
- SourceLocation,
8
- TemplateKey,
9
- TranslationTemplate,
10
- } from './types.ts';
11
-
12
- const IGNORED_DIRECTORIES = new Set([
13
- 'node_modules',
14
- '.git',
15
- 'build',
16
- 'dist',
17
- 'Pods',
18
- ]);
19
-
20
- /** Converts a glob (`**`, `*`, `?`, `{a,b}`) to an anchored RegExp. */
21
- export function globToRegExp(glob: string): RegExp {
22
- let source = '';
23
- let index = 0;
24
- while (index < glob.length) {
25
- const char = glob[index];
26
- if (char === '*') {
27
- if (glob[index + 1] === '*') {
28
- // `**/` matches zero or more directories; trailing `**` matches all.
29
- if (glob[index + 2] === '/') {
30
- source += '(?:.*/)?';
31
- index += 3;
32
- } else {
33
- source += '.*';
34
- index += 2;
35
- }
36
- continue;
37
- }
38
- source += '[^/]*';
39
- } else if (char === '?') {
40
- source += '[^/]';
41
- } else if (char === '{') {
42
- const end = glob.indexOf('}', index);
43
- if (end === -1) {
44
- source += '\\{';
45
- } else {
46
- const alternatives = glob
47
- .slice(index + 1, end)
48
- .split(',')
49
- .map(alternative =>
50
- alternative.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
51
- );
52
- source += `(?:${alternatives.join('|')})`;
53
- index = end;
54
- }
55
- } else {
56
- source += char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
57
- }
58
- index += 1;
59
- }
60
- return new RegExp(`^${source}$`);
61
- }
62
-
63
- function walkDirectory(rootDir: string, current: string, files: string[]): void {
64
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
65
- if (entry.isDirectory()) {
66
- if (!IGNORED_DIRECTORIES.has(entry.name)) {
67
- walkDirectory(rootDir, path.join(current, entry.name), files);
68
- }
69
- continue;
70
- }
71
- if (entry.isFile()) {
72
- files.push(
73
- path.relative(rootDir, path.join(current, entry.name)).replace(/\\/g, '/'),
74
- );
75
- }
76
- }
77
- }
78
-
79
- /** Lists project-relative source files matching the configured globs. */
80
- export function listSourceFiles(
81
- rootDir: string,
82
- sourceGlobs: readonly string[],
83
- ): string[] {
84
- const allFiles: string[] = [];
85
- walkDirectory(rootDir, rootDir, allFiles);
86
- const matchers = sourceGlobs.map(globToRegExp);
87
- return allFiles
88
- .filter(file => matchers.some(matcher => matcher.test(file)))
89
- .sort();
90
- }
91
-
92
- function lineOfIndex(text: string, index: number): number {
93
- let line = 1;
94
- for (let position = 0; position < index && position < text.length; position += 1) {
95
- if (text[position] === '\n') {
96
- line += 1;
97
- }
98
- }
99
- return line;
100
- }
101
-
102
- interface CompiledPattern {
103
- keyRegex: RegExp;
104
- ambiguousRegex?: RegExp;
105
- }
106
-
107
- function compilePatterns(patterns: readonly PatternConfig[]): CompiledPattern[] {
108
- return patterns.map(pattern => {
109
- const flags = [...new Set(`g${pattern.flags ?? ''}`)].join('');
110
- return {
111
- keyRegex: new RegExp(pattern.regex, flags),
112
- ambiguousRegex: pattern.ambiguousRegex
113
- ? new RegExp(pattern.ambiguousRegex, flags)
114
- : undefined,
115
- };
116
- });
117
- }
118
-
119
- /**
120
- * Scans the source tree with the app-configured i18n usage regex patterns and
121
- * builds the source-of-truth translation template: discovered keys with
122
- * default text and `file:line` locations, ambiguous dynamic usages, and
123
- * conflicting duplicate keys (FR-024…FR-026, FR-031).
124
- */
125
- export function scanSource(
126
- rootDir: string,
127
- sourceGlobs: readonly string[],
128
- patterns: readonly PatternConfig[],
129
- ): TranslationTemplate {
130
- const compiled = compilePatterns(patterns);
131
- const files = listSourceFiles(rootDir, sourceGlobs);
132
- const keys = new Map<string, TemplateKey>();
133
- const valuesByKey = new Map<string, { text: string; location: SourceLocation }[]>();
134
- const ambiguous: AmbiguousUsage[] = [];
135
-
136
- for (const file of files) {
137
- const text = fs.readFileSync(path.join(rootDir, file), 'utf8');
138
- const matchedSpans: [number, number][] = [];
139
-
140
- for (const pattern of compiled) {
141
- pattern.keyRegex.lastIndex = 0;
142
- for (const match of text.matchAll(pattern.keyRegex)) {
143
- const key = match.groups?.key;
144
- if (!key) {
145
- continue;
146
- }
147
- const startIndex = match.index ?? 0;
148
- matchedSpans.push([startIndex, startIndex + match[0].length]);
149
- const location: SourceLocation = {
150
- file,
151
- line: lineOfIndex(text, startIndex),
152
- };
153
- const existing = keys.get(key);
154
- if (existing) {
155
- existing.locations.push(location);
156
- } else {
157
- keys.set(key, { key, locations: [location] });
158
- }
159
- const defaultText = match.groups?.defaultText;
160
- if (defaultText !== undefined) {
161
- const values = valuesByKey.get(key) ?? [];
162
- values.push({ text: defaultText, location });
163
- valuesByKey.set(key, values);
164
- }
165
- }
166
- }
167
-
168
- for (const pattern of compiled) {
169
- if (!pattern.ambiguousRegex) {
170
- continue;
171
- }
172
- pattern.ambiguousRegex.lastIndex = 0;
173
- for (const match of text.matchAll(pattern.ambiguousRegex)) {
174
- const start = match.index ?? 0;
175
- const overlapsKeyMatch = matchedSpans.some(
176
- ([from, to]) => start >= from && start < to,
177
- );
178
- if (overlapsKeyMatch) {
179
- continue;
180
- }
181
- const lineNumber = lineOfIndex(text, start);
182
- ambiguous.push({
183
- file,
184
- line: lineNumber,
185
- snippet: text.split('\n')[lineNumber - 1]?.trim() ?? match[0],
186
- });
187
- }
188
- }
189
- }
190
-
191
- const conflicts: KeyConflict[] = [];
192
- for (const [key, values] of valuesByKey) {
193
- const distinct = [...new Set(values.map(value => value.text))];
194
- if (distinct.length > 1) {
195
- conflicts.push({ key, values });
196
- } else {
197
- const templateKey = keys.get(key);
198
- if (templateKey) {
199
- templateKey.defaultText = values[0].text;
200
- }
201
- }
202
- }
203
-
204
- return {
205
- keys: [...keys.values()].sort((a, b) => a.key.localeCompare(b.key)),
206
- ambiguous,
207
- conflicts: conflicts.sort((a, b) => a.key.localeCompare(b.key)),
208
- scannedFiles: files.length,
209
- };
210
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import type {
4
+ AmbiguousUsage,
5
+ KeyConflict,
6
+ PatternConfig,
7
+ SourceLocation,
8
+ TemplateKey,
9
+ TranslationTemplate,
10
+ } from './types.ts';
11
+
12
+ const IGNORED_DIRECTORIES = new Set(['node_modules', '.git', 'build', 'dist', 'Pods']);
13
+
14
+ /** Converts a glob (`**`, `*`, `?`, `{a,b}`) to an anchored RegExp. */
15
+ export function globToRegExp(glob: string): RegExp {
16
+ let source = '';
17
+ let index = 0;
18
+ while (index < glob.length) {
19
+ const char = glob[index];
20
+ if (char === '*') {
21
+ if (glob[index + 1] === '*') {
22
+ // `**/` matches zero or more directories; trailing `**` matches all.
23
+ if (glob[index + 2] === '/') {
24
+ source += '(?:.*/)?';
25
+ index += 3;
26
+ } else {
27
+ source += '.*';
28
+ index += 2;
29
+ }
30
+ continue;
31
+ }
32
+ source += '[^/]*';
33
+ } else if (char === '?') {
34
+ source += '[^/]';
35
+ } else if (char === '{') {
36
+ const end = glob.indexOf('}', index);
37
+ if (end === -1) {
38
+ source += '\\{';
39
+ } else {
40
+ const alternatives = glob
41
+ .slice(index + 1, end)
42
+ .split(',')
43
+ .map(alternative => alternative.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
44
+ source += `(?:${alternatives.join('|')})`;
45
+ index = end;
46
+ }
47
+ } else {
48
+ source += char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
49
+ }
50
+ index += 1;
51
+ }
52
+ return new RegExp(`^${source}$`);
53
+ }
54
+
55
+ function walkDirectory(rootDir: string, current: string, files: string[]): void {
56
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
57
+ if (entry.isDirectory()) {
58
+ if (!IGNORED_DIRECTORIES.has(entry.name)) {
59
+ walkDirectory(rootDir, path.join(current, entry.name), files);
60
+ }
61
+ continue;
62
+ }
63
+ if (entry.isFile()) {
64
+ files.push(path.relative(rootDir, path.join(current, entry.name)).replace(/\\/g, '/'));
65
+ }
66
+ }
67
+ }
68
+
69
+ /** Lists project-relative source files matching the configured globs. */
70
+ export function listSourceFiles(rootDir: string, sourceGlobs: readonly string[]): string[] {
71
+ const allFiles: string[] = [];
72
+ walkDirectory(rootDir, rootDir, allFiles);
73
+ const matchers = sourceGlobs.map(globToRegExp);
74
+ return allFiles.filter(file => matchers.some(matcher => matcher.test(file))).sort();
75
+ }
76
+
77
+ function lineOfIndex(text: string, index: number): number {
78
+ let line = 1;
79
+ for (let position = 0; position < index && position < text.length; position += 1) {
80
+ if (text[position] === '\n') {
81
+ line += 1;
82
+ }
83
+ }
84
+ return line;
85
+ }
86
+
87
+ interface CompiledPattern {
88
+ keyRegex: RegExp;
89
+ ambiguousRegex?: RegExp;
90
+ }
91
+
92
+ function compilePatterns(patterns: readonly PatternConfig[]): CompiledPattern[] {
93
+ return patterns.map(pattern => {
94
+ const flags = [...new Set(`g${pattern.flags ?? ''}`)].join('');
95
+ return {
96
+ keyRegex: new RegExp(pattern.regex, flags),
97
+ ambiguousRegex: pattern.ambiguousRegex
98
+ ? new RegExp(pattern.ambiguousRegex, flags)
99
+ : undefined,
100
+ };
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Scans the source tree with the app-configured i18n usage regex patterns and
106
+ * builds the source-of-truth translation template: discovered keys with
107
+ * default text and `file:line` locations, ambiguous dynamic usages, and
108
+ * conflicting duplicate keys (FR-024…FR-026, FR-031).
109
+ */
110
+ export function scanSource(
111
+ rootDir: string,
112
+ sourceGlobs: readonly string[],
113
+ patterns: readonly PatternConfig[],
114
+ ): TranslationTemplate {
115
+ const compiled = compilePatterns(patterns);
116
+ const files = listSourceFiles(rootDir, sourceGlobs);
117
+ const keys = new Map<string, TemplateKey>();
118
+ const valuesByKey = new Map<string, { text: string; location: SourceLocation }[]>();
119
+ const ambiguous: AmbiguousUsage[] = [];
120
+
121
+ for (const file of files) {
122
+ const text = fs.readFileSync(path.join(rootDir, file), 'utf8');
123
+ const matchedSpans: [number, number][] = [];
124
+
125
+ for (const pattern of compiled) {
126
+ pattern.keyRegex.lastIndex = 0;
127
+ for (const match of text.matchAll(pattern.keyRegex)) {
128
+ const key = match.groups?.key;
129
+ if (!key) {
130
+ continue;
131
+ }
132
+ const startIndex = match.index ?? 0;
133
+ matchedSpans.push([startIndex, startIndex + match[0].length]);
134
+ const location: SourceLocation = {
135
+ file,
136
+ line: lineOfIndex(text, startIndex),
137
+ };
138
+ const existing = keys.get(key);
139
+ if (existing) {
140
+ existing.locations.push(location);
141
+ } else {
142
+ keys.set(key, { key, locations: [location] });
143
+ }
144
+ const defaultText = match.groups?.defaultText;
145
+ if (defaultText !== undefined) {
146
+ const values = valuesByKey.get(key) ?? [];
147
+ values.push({ text: defaultText, location });
148
+ valuesByKey.set(key, values);
149
+ }
150
+ }
151
+ }
152
+
153
+ for (const pattern of compiled) {
154
+ if (!pattern.ambiguousRegex) {
155
+ continue;
156
+ }
157
+ pattern.ambiguousRegex.lastIndex = 0;
158
+ for (const match of text.matchAll(pattern.ambiguousRegex)) {
159
+ const start = match.index ?? 0;
160
+ const overlapsKeyMatch = matchedSpans.some(([from, to]) => start >= from && start < to);
161
+ if (overlapsKeyMatch) {
162
+ continue;
163
+ }
164
+ const lineNumber = lineOfIndex(text, start);
165
+ ambiguous.push({
166
+ file,
167
+ line: lineNumber,
168
+ snippet: text.split('\n')[lineNumber - 1]?.trim() ?? match[0],
169
+ });
170
+ }
171
+ }
172
+ }
173
+
174
+ const conflicts: KeyConflict[] = [];
175
+ for (const [key, values] of valuesByKey) {
176
+ const distinct = [...new Set(values.map(value => value.text))];
177
+ if (distinct.length > 1) {
178
+ conflicts.push({ key, values });
179
+ } else {
180
+ const templateKey = keys.get(key);
181
+ if (templateKey) {
182
+ templateKey.defaultText = values[0].text;
183
+ }
184
+ }
185
+ }
186
+
187
+ return {
188
+ keys: [...keys.values()].sort((a, b) => a.key.localeCompare(b.key)),
189
+ ambiguous,
190
+ conflicts: conflicts.sort((a, b) => a.key.localeCompare(b.key)),
191
+ scannedFiles: files.length,
192
+ };
193
+ }