@babelize/engine 0.1.3 → 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.
Files changed (50) hide show
  1. package/dist/config.d.ts +3 -3
  2. package/dist/detection/stack.d.ts +3 -1
  3. package/dist/detection/stack.js +13 -6
  4. package/dist/injection/StrategyFactory.d.ts +5 -1
  5. package/dist/injection/StrategyFactory.js +7 -2
  6. package/dist/injection/strategies/VanillaStrategy.d.ts +8 -0
  7. package/dist/injection/strategies/VanillaStrategy.js +10 -3
  8. package/dist/local/translate-core.d.ts +1 -1
  9. package/dist/local/translate-core.js +1 -1
  10. package/dist/modules/extractor/extract.js +67 -10
  11. package/dist/modules/finalization/dependencies.d.ts +10 -0
  12. package/dist/modules/finalization/dependencies.js +23 -0
  13. package/dist/modules/finalization/finalize.js +41 -39
  14. package/dist/modules/finalization/nextjs-finalize.js +2 -1
  15. package/dist/modules/finalization/react-native-finalize.d.ts +45 -0
  16. package/dist/modules/finalization/react-native-finalize.js +324 -0
  17. package/dist/modules/format-injection/index.js +38 -9
  18. package/dist/modules/format-injection/inject-formats.d.ts +3 -2
  19. package/dist/modules/format-injection/inject-formats.js +4 -3
  20. package/dist/modules/injection/inject.d.ts +14 -1
  21. package/dist/modules/injection/inject.js +307 -154
  22. package/dist/modules/sync-detector/sync-detector.js +11 -4
  23. package/dist/modules/translator/line-breaks.d.ts +8 -0
  24. package/dist/modules/translator/line-breaks.js +63 -0
  25. package/dist/modules/translator/translate.js +5 -2
  26. package/dist/modules/workspace/validate.d.ts +13 -1
  27. package/dist/modules/workspace/validate.js +95 -58
  28. package/dist/runtime/types.d.ts +22 -0
  29. package/dist/runtime/types.js +33 -0
  30. package/dist/utils/ast-helpers.d.ts +30 -2
  31. package/dist/utils/ast-helpers.js +355 -42
  32. package/dist/utils/extraction-options.js +1 -1
  33. package/dist/utils/file-utils.js +4 -1
  34. package/dist/utils/i18n-config-path.d.ts +18 -0
  35. package/dist/utils/i18n-config-path.js +64 -0
  36. package/dist/utils/i18next-icu.js +1 -1
  37. package/dist/utils/jsx-message.d.ts +16 -1
  38. package/dist/utils/jsx-message.js +89 -9
  39. package/dist/utils/key-usage.js +1 -1
  40. package/dist/utils/metadata.d.ts +5 -1
  41. package/dist/utils/metadata.js +19 -1
  42. package/dist/utils/module-constants.d.ts +21 -3
  43. package/dist/utils/module-constants.js +123 -25
  44. package/dist/utils/react-native.d.ts +17 -0
  45. package/dist/utils/react-native.js +108 -0
  46. package/dist/utils/string-utils.d.ts +8 -0
  47. package/dist/utils/string-utils.js +29 -1
  48. package/dist/utils/ui-copy.d.ts +5 -4
  49. package/dist/utils/ui-copy.js +10 -5
  50. package/package.json +3 -2
package/dist/config.d.ts CHANGED
@@ -4,7 +4,7 @@ declare const configSchema: z.ZodObject<{
4
4
  workspacePath: z.ZodString;
5
5
  sourceLocale: z.ZodDefault<z.ZodString>;
6
6
  targetLocales: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
7
- framework: z.ZodOptional<z.ZodEnum<["react", "nextjs", "vue", "static", "unknown", "django"]>>;
7
+ framework: z.ZodOptional<z.ZodEnum<["react", "nextjs", "vue", "static", "unknown", "django", "react-native"]>>;
8
8
  dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
9
9
  projectId: z.ZodOptional<z.ZodString>;
10
10
  sourceDir: z.ZodOptional<z.ZodString>;
@@ -40,7 +40,7 @@ declare const configSchema: z.ZodObject<{
40
40
  sourceLocale: string;
41
41
  targetLocales: string[];
42
42
  dryRun: boolean;
43
- framework?: "react" | "nextjs" | "vue" | "static" | "unknown" | "django" | undefined;
43
+ framework?: "react" | "nextjs" | "vue" | "static" | "unknown" | "django" | "react-native" | undefined;
44
44
  projectId?: string | undefined;
45
45
  sourceDir?: string | undefined;
46
46
  localesDir?: string | undefined;
@@ -56,7 +56,7 @@ declare const configSchema: z.ZodObject<{
56
56
  workspacePath: string;
57
57
  sourceLocale?: string | undefined;
58
58
  targetLocales?: string[] | undefined;
59
- framework?: "react" | "nextjs" | "vue" | "static" | "unknown" | "django" | undefined;
59
+ framework?: "react" | "nextjs" | "vue" | "static" | "unknown" | "django" | "react-native" | undefined;
60
60
  dryRun?: boolean | undefined;
61
61
  projectId?: string | undefined;
62
62
  sourceDir?: string | undefined;
@@ -9,7 +9,7 @@
9
9
  * is React is a React project for Babelize). An unsupported stack is only
10
10
  * reported when nothing Babelize can translate was found.
11
11
  */
12
- export type StackFramework = 'react' | 'nextjs' | 'vue' | 'static' | 'django' | 'unknown';
12
+ export type StackFramework = 'react' | 'nextjs' | 'vue' | 'static' | 'django' | 'react-native' | 'unknown';
13
13
  export type PackageManagerId = 'bun' | 'pnpm' | 'yarn' | 'npm' | 'uv' | 'poetry' | 'pipenv' | 'pdm' | 'pip';
14
14
  export interface DetectedStack {
15
15
  framework: StackFramework;
@@ -36,6 +36,8 @@ export interface RepoFiles {
36
36
  export declare const FRAMEWORK_DISPLAY_NAMES: Record<string, string>;
37
37
  /** Does a Python dependency file list Django itself (not just django-* add-ons)? */
38
38
  export declare function mentionsPythonPackage(content: string, pkg: string): boolean;
39
+ /** Human list of supported stacks, for messages. */
40
+ export declare const SUPPORTED_SUMMARY = "React, Next.js, Vue, React Native, Django and plain HTML/CSS/JS";
39
41
  export declare function detectPackageManagers(paths: Set<string>, pyproject: string | null, preferPython: boolean): PackageManagerId[];
40
42
  export declare function detectStack(files: RepoFiles): Promise<DetectedStack>;
41
43
  /** RepoFiles for a local directory. */
@@ -47,7 +47,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
47
47
  return (mod && mod.__esModule) ? mod : { "default": mod };
48
48
  };
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.FRAMEWORK_DISPLAY_NAMES = void 0;
50
+ exports.SUPPORTED_SUMMARY = exports.FRAMEWORK_DISPLAY_NAMES = void 0;
51
51
  exports.mentionsPythonPackage = mentionsPythonPackage;
52
52
  exports.detectPackageManagers = detectPackageManagers;
53
53
  exports.detectStack = detectStack;
@@ -56,7 +56,8 @@ const fs = __importStar(require("fs"));
56
56
  const path = __importStar(require("path"));
57
57
  const fast_glob_1 = __importDefault(require("fast-glob"));
58
58
  exports.FRAMEWORK_DISPLAY_NAMES = {
59
- react: 'React', nextjs: 'Next.js', vue: 'Vue', static: 'HTML/CSS/JS', django: 'Django', unknown: 'Unknown',
59
+ react: 'React', nextjs: 'Next.js', vue: 'Vue', static: 'HTML/CSS/JS', django: 'Django',
60
+ 'react-native': 'React Native', unknown: 'Unknown',
60
61
  };
61
62
  const IGNORED_DIR = /(?:^|\/)(?:node_modules|\.git|venv|\.venv|env|site-packages|vendor|dist|build|\.next|__pycache__)\//;
62
63
  function depth(p) {
@@ -75,6 +76,9 @@ function jsFramework(pkg) {
75
76
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
76
77
  if (deps['next'])
77
78
  return { id: 'nextjs' };
79
+ // Before react: Expo / React Native apps depend on react (and Expo Router on react-dom).
80
+ if (deps['expo'] || deps['react-native'])
81
+ return { id: 'react-native' };
78
82
  if (deps['nuxt'] || deps['vue'])
79
83
  return { id: 'vue' };
80
84
  if (deps['react'] || deps['gatsby'] || deps['@remix-run/react'] || deps['astro'] || deps['react-dom'] || deps['react-scripts'])
@@ -89,6 +93,9 @@ function jsFramework(pkg) {
89
93
  return { id: 'ember' };
90
94
  return { id: 'unknown' };
91
95
  }
96
+ const SUPPORTED_JS = new Set(['nextjs', 'vue', 'react', 'react-native']);
97
+ /** Human list of supported stacks, for messages. */
98
+ exports.SUPPORTED_SUMMARY = 'React, Next.js, Vue, React Native, Django and plain HTML/CSS/JS';
92
99
  const UNSUPPORTED_NAMES = {
93
100
  angular: 'Angular', svelte: 'Svelte', solid: 'SolidJS', ember: 'Ember', laravel: 'Laravel', symfony: 'Symfony',
94
101
  wordpress: 'WordPress', php: 'PHP', rails: 'Ruby on Rails', jekyll: 'Jekyll', ruby: 'Ruby', flutter: 'Flutter',
@@ -151,7 +158,7 @@ async function detectStack(files) {
151
158
  supported: false,
152
159
  unsupported: { id, name },
153
160
  packageManagers: detectPackageManagers(set, pyproject, preferPython),
154
- message: `${name} isn't supported yet. Babelize supports React, Next.js, Vue, Django and plain HTML/CSS/JS projects.`,
161
+ message: `${name} isn't supported yet. Babelize supports ${exports.SUPPORTED_SUMMARY} projects.`,
155
162
  };
156
163
  };
157
164
  // 1. Django at the root.
@@ -160,7 +167,7 @@ async function detectStack(files) {
160
167
  // 2. A JS framework at the root.
161
168
  const rootPkg = has('package.json') ? await readJson(files, 'package.json') : null;
162
169
  const rootJs = jsFramework(rootPkg);
163
- if (rootJs.id === 'nextjs' || rootJs.id === 'vue' || rootJs.id === 'react')
170
+ if (SUPPORTED_JS.has(rootJs.id))
164
171
  return result(rootJs.id);
165
172
  // 3. Django deeper in the tree, or declared as a dependency.
166
173
  const pyDepFiles = ['requirements.txt', 'requirements/base.txt', 'requirements/prod.txt', 'requirements/production.txt', 'Pipfile', 'setup.py', 'setup.cfg']
@@ -173,7 +180,7 @@ async function detectStack(files) {
173
180
  const subPkgs = paths.filter((p) => p.endsWith('/package.json') && depth(p) <= 2).slice(0, 25);
174
181
  for (const p of subPkgs) {
175
182
  const sub = jsFramework(await readJson(files, p));
176
- if (sub.id === 'nextjs' || sub.id === 'vue' || sub.id === 'react')
183
+ if (SUPPORTED_JS.has(sub.id))
177
184
  return result(sub.id);
178
185
  }
179
186
  // 5. Unsupported stacks with a clear signal.
@@ -247,7 +254,7 @@ async function detectStack(files) {
247
254
  packageManagers: detectPackageManagers(set, pyproject, false),
248
255
  message: files.truncated
249
256
  ? "This repository is too large to inspect before creating the project."
250
- : 'No React, Next.js, Vue, Django or HTML files were found in this repository.',
257
+ : 'No React, Next.js, Vue, React Native, Django or HTML files were found in this repository.',
251
258
  };
252
259
  }
253
260
  /** RepoFiles for a local directory. */
@@ -1,4 +1,8 @@
1
1
  import { InjectionStrategy } from './InjectionStrategy';
2
2
  export declare class StrategyFactory {
3
- static getStrategy(framework: string, existingDeps?: Record<string, string>): InjectionStrategy;
3
+ /**
4
+ * @param moduleTranslator Import path of the i18n config's `t` for the
5
+ * vanilla strategy (relative to the file being injected).
6
+ */
7
+ static getStrategy(framework: string, existingDeps?: Record<string, string>, moduleTranslator?: string): InjectionStrategy;
4
8
  }
@@ -6,7 +6,11 @@ const NextIntlStrategy_1 = require("./strategies/NextIntlStrategy");
6
6
  const VanillaStrategy_1 = require("./strategies/VanillaStrategy");
7
7
  const VueI18nStrategy_1 = require("./strategies/VueI18nStrategy");
8
8
  class StrategyFactory {
9
- static getStrategy(framework, existingDeps) {
9
+ /**
10
+ * @param moduleTranslator Import path of the i18n config's `t` for the
11
+ * vanilla strategy (relative to the file being injected).
12
+ */
13
+ static getStrategy(framework, existingDeps, moduleTranslator) {
10
14
  if (existingDeps?.['react-i18next']) {
11
15
  return new ReactI18nextStrategy_1.ReactI18nextStrategy();
12
16
  }
@@ -17,12 +21,13 @@ class StrategyFactory {
17
21
  case 'nextjs':
18
22
  return new NextIntlStrategy_1.NextIntlStrategy();
19
23
  case 'react':
24
+ case 'react-native':
20
25
  return new ReactI18nextStrategy_1.ReactI18nextStrategy();
21
26
  case 'vue':
22
27
  return new VueI18nStrategy_1.VueI18nStrategy();
23
28
  case 'unknown':
24
29
  default:
25
- return new VanillaStrategy_1.VanillaStrategy();
30
+ return new VanillaStrategy_1.VanillaStrategy(moduleTranslator);
26
31
  }
27
32
  }
28
33
  }
@@ -1,7 +1,15 @@
1
1
  import * as t from '@babel/types';
2
2
  import { InjectionStrategy, InjectionContext } from '../InjectionStrategy';
3
+ /**
4
+ * Plain `t()` with no hook: module-level code, and files without a component
5
+ * the hook can go in. `t` is imported from the project's i18n config, by
6
+ * default the `@/i18n` alias; injection passes the relative path to
7
+ * Babelize's generated config when it knows it (Metro has no `@/` alias).
8
+ */
3
9
  export declare class VanillaStrategy implements InjectionStrategy {
10
+ private readonly source;
4
11
  id: string;
12
+ constructor(source?: string);
5
13
  hasImport(ast: t.File): boolean;
6
14
  createImport(): t.ImportDeclaration;
7
15
  hasHook(_componentPath: any): boolean;
@@ -35,14 +35,21 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.VanillaStrategy = void 0;
37
37
  const t = __importStar(require("@babel/types"));
38
+ /**
39
+ * Plain `t()` with no hook: module-level code, and files without a component
40
+ * the hook can go in. `t` is imported from the project's i18n config, by
41
+ * default the `@/i18n` alias; injection passes the relative path to
42
+ * Babelize's generated config when it knows it (Metro has no `@/` alias).
43
+ */
38
44
  class VanillaStrategy {
39
- constructor() {
45
+ constructor(source = '@/i18n') {
46
+ this.source = source;
40
47
  this.id = 'vanilla';
41
48
  }
42
49
  hasImport(ast) {
43
50
  let found = false;
44
51
  for (const node of ast.program.body) {
45
- if (t.isImportDeclaration(node) && node.source.value === '@/i18n') {
52
+ if (t.isImportDeclaration(node) && node.source.value === this.source) {
46
53
  const hasSpecifier = node.specifiers.some(spec => t.isImportSpecifier(spec) &&
47
54
  t.isIdentifier(spec.imported) &&
48
55
  spec.imported.name === 't');
@@ -55,7 +62,7 @@ class VanillaStrategy {
55
62
  return found;
56
63
  }
57
64
  createImport() {
58
- return t.importDeclaration([t.importSpecifier(t.identifier('t'), t.identifier('t'))], t.stringLiteral('@/i18n'));
65
+ return t.importDeclaration([t.importSpecifier(t.identifier('t'), t.identifier('t'))], t.stringLiteral(this.source));
59
66
  }
60
67
  hasHook(_componentPath) {
61
68
  return true; // No hook needed for vanilla JS
@@ -11,7 +11,7 @@ import { type FileDiffStat, type ApplyIo, type WorkspaceChanges } from './worksp
11
11
  export declare const LOCAL_PROJECT_ID = "local";
12
12
  export declare const LOCAL_LOCKFILE = ".babelize/local/babelize-lock.json";
13
13
  export declare const SUPPORTED_LOCAL_FRAMEWORKS: readonly ["react", "nextjs", "django"];
14
- export declare const BETA_LOCAL_FRAMEWORKS: readonly ["vue", "static"];
14
+ export declare const BETA_LOCAL_FRAMEWORKS: readonly ["vue", "static", "react-native"];
15
15
  export interface LocalTranslateOptions {
16
16
  projectDir: string;
17
17
  sourceLocale: string;
@@ -73,7 +73,7 @@ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
73
73
  exports.LOCAL_PROJECT_ID = 'local';
74
74
  exports.LOCAL_LOCKFILE = `.babelize/${exports.LOCAL_PROJECT_ID}/babelize-lock.json`;
75
75
  exports.SUPPORTED_LOCAL_FRAMEWORKS = ['react', 'nextjs', 'django'];
76
- exports.BETA_LOCAL_FRAMEWORKS = ['vue', 'static'];
76
+ exports.BETA_LOCAL_FRAMEWORKS = ['vue', 'static', 'react-native'];
77
77
  class LocalTranslateError extends Error {
78
78
  constructor(message, code,
79
79
  /** Files left changed in the project (only for a failed, un-rolled-back apply). */
@@ -47,8 +47,9 @@ const types_2 = require("@babelize/types");
47
47
  const jsx_message_1 = require("../../utils/jsx-message");
48
48
  const ui_copy_1 = require("../../utils/ui-copy");
49
49
  const module_constants_1 = require("../../utils/module-constants");
50
+ const react_native_1 = require("../../utils/react-native");
50
51
  /** Frameworks whose injection strategies can render a JSX sentence as one message. */
51
- const SENTENCE_FRAMEWORKS = new Set(['react', 'nextjs']);
52
+ const SENTENCE_FRAMEWORKS = new Set(['react', 'nextjs', 'react-native']);
52
53
  async function extractStrings(workspace, filesToProcess, extractOptions = {}) {
53
54
  const detectFormats = !!workspace.formatConfig?.enabled;
54
55
  const strings = [];
@@ -64,11 +65,12 @@ async function extractStrings(workspace, filesToProcess, extractOptions = {}) {
64
65
  }
65
66
  }
66
67
  else {
67
- // Otherwise scan all source directories. Django scans .py/.html and
68
- // skips virtualenvs, migrations, static files and the like.
69
- const fwConfig = workspace.framework === 'django' ? types_2.FRAMEWORK_CONFIGS.django : undefined;
68
+ // Otherwise scan all source directories. Root-scanning frameworks use
69
+ // their own extensions (Django .py/.html, React Native only JS/TS), and
70
+ // every framework's exclusions (virtualenvs, native ios/android projects).
71
+ const fwConfig = types_2.FRAMEWORK_CONFIGS[workspace.framework]?.scanRoot ? types_2.FRAMEWORK_CONFIGS[workspace.framework] : undefined;
70
72
  const extensions = fwConfig ? [...fwConfig.extensions] : types_1.SUPPORTED_EXTENSIONS;
71
- const excluded = fwConfig ? [...types_1.EXCLUDED_PATTERNS, ...(fwConfig.excludedPatterns ?? [])] : types_1.EXCLUDED_PATTERNS;
73
+ const excluded = (0, types_1.workspaceExcludes)(workspace);
72
74
  for (const sourceDir of workspace.sourceDirs) {
73
75
  const dirPath = path.join(workspace.path, sourceDir);
74
76
  const files = await (0, file_utils_1.findFiles)(dirPath, extensions, excluded);
@@ -152,6 +154,9 @@ async function extractStringsFromFile(content, filePath, detectFormats, options)
152
154
  // into a sentence message — not extracted again on their own.
153
155
  const consumed = new WeakSet();
154
156
  const unwired = [];
157
+ // React Native copy outside JSX text: screen titles in navigation options,
158
+ // Alert titles/messages, two-letter button labels ("OK").
159
+ const reactNative = options?.framework === 'react-native';
155
160
  const isTypeScript = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
156
161
  try {
157
162
  const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript });
@@ -162,7 +167,9 @@ async function extractStringsFromFile(content, filePath, detectFormats, options)
162
167
  for (const lit of plan.literals) {
163
168
  const text = lit.node.value.trim();
164
169
  const line = lit.node.loc?.start.line || 0;
165
- if (!plan.provable) {
170
+ if (!plan.provable && !plan.labels) {
171
+ // Reported, and not translated in place either: the values are also data.
172
+ consumed.add(lit.node);
166
173
  unwired.push({
167
174
  file: filePath,
168
175
  key: '',
@@ -202,11 +209,25 @@ async function extractStringsFromFile(content, filePath, detectFormats, options)
202
209
  return;
203
210
  if ((0, ast_helpers_1.isNonTranslatableContext)(path))
204
211
  return;
205
- if (path.parentPath && path.parentPath.isCallExpression() && path.parentPath.node.arguments.includes(path.node)) {
212
+ // `rgba(255,255,255,0.72)` would otherwise pass as a currency amount below.
213
+ if ((0, string_utils_1.looksLikeColor)(text))
206
214
  return;
215
+ // Never shown: an object key (`{ 'in-review': … }`), a type (`useState<"active" | "all">`),
216
+ // or a machine date (`placeholder="2026-05-30"` is the format the field parses).
217
+ if (isKeyOrType(path))
218
+ return;
219
+ if (/^\d{4}-\d{2}-\d{2}(?:[T ][\d:.]+Z?)?$/.test(text))
220
+ return;
221
+ // A literal in a branch (`role === 'brand' ? 'brand' : 'creator'`, `user?.id ?? 'usr_1'`)
222
+ // goes where the whole expression goes, so that's where it's judged.
223
+ const site = valueSite(path);
224
+ if (site.parentPath && site.parentPath.isCallExpression() && site.parentPath.node.arguments.includes(site.node)) {
225
+ // Call arguments are program data, except an Alert dialog's title and message.
226
+ if (!(reactNative && (0, react_native_1.isAlertTextArgument)(site)))
227
+ return;
207
228
  }
208
229
  // Program data in initializers (`{ name: "Ada" }`, `href: "/"`) is not UI copy.
209
- if (!(0, string_utils_1.matchesCurrencyPattern)(text) && !(0, string_utils_1.matchesDatePattern)(text) && !(0, ui_copy_1.isUiCopyLiteral)(path.node, path.parent, text))
230
+ if (!(0, string_utils_1.matchesCurrencyPattern)(text) && !(0, string_utils_1.matchesDatePattern)(text) && !(0, ui_copy_1.isUiCopyLiteral)(site.node, site.parent, text))
210
231
  return;
211
232
  const format = detectFormats ? (0, detect_format_1.detectFormat)(text) : null;
212
233
  if (format) {
@@ -250,7 +271,9 @@ async function extractStringsFromFile(content, filePath, detectFormats, options)
250
271
  while (parent) {
251
272
  if (parent.isJSXAttribute()) {
252
273
  const nameNode = parent.node.name;
253
- if (ast_helpers_1.t.isJSXIdentifier(nameNode) && !types_1.JSX_TRANSLATABLE_ATTRS.includes(nameNode.name)) {
274
+ if (ast_helpers_1.t.isJSXIdentifier(nameNode) &&
275
+ !types_1.JSX_TRANSLATABLE_ATTRS.includes(nameNode.name) &&
276
+ !(reactNative && (0, react_native_1.isNavigationTitle)(path))) {
254
277
  return;
255
278
  }
256
279
  break;
@@ -264,7 +287,8 @@ async function extractStringsFromFile(content, filePath, detectFormats, options)
264
287
  const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
265
288
  const hasDate = (0, string_utils_1.matchesDatePattern)(text);
266
289
  const isDateOrCurrencyValue = (hasCurrency || hasDate) && text.length >= 3 && !(0, string_utils_1.containsPII)(text);
267
- if (!(0, string_utils_1.isTranslatableText)(text) || (0, string_utils_1.containsPII)(text) || text.length < 3) {
290
+ const tooShort = text.length < 3 && !(reactNative && (0, react_native_1.isShortUiWord)(text));
291
+ if (!(0, string_utils_1.isTranslatableText)(text) || (0, string_utils_1.containsPII)(text) || tooShort) {
268
292
  if (!isDateOrCurrencyValue && !format)
269
293
  return;
270
294
  }
@@ -443,6 +467,8 @@ async function extractStringsFromFile(content, filePath, detectFormats, options)
443
467
  if ((0, ast_helpers_1.isNonTranslatableContext)(path))
444
468
  return;
445
469
  const text = path.node.value.value.trim();
470
+ if ((0, string_utils_1.looksLikeColor)(text))
471
+ return;
446
472
  if (!(0, string_utils_1.matchesCurrencyPattern)(text) && !(0, string_utils_1.matchesDatePattern)(text) && !(0, ui_copy_1.isUiCopyLiteral)(path.node.value, path.node, text))
447
473
  return;
448
474
  const hasCurrency = (0, string_utils_1.matchesCurrencyPattern)(text);
@@ -548,6 +574,37 @@ async function extractStringsFromFile(content, filePath, detectFormats, options)
548
574
  }
549
575
  return { strings, warnings, notInjected: unwired };
550
576
  }
577
+ /** A string that names something rather than being shown: a property key, or part of a TypeScript type. */
578
+ function isKeyOrType(path) {
579
+ const parent = path.parentPath;
580
+ if (!parent)
581
+ return false;
582
+ const keyed = parent.isObjectProperty() || parent.isObjectMethod() || parent.isClassProperty() || parent.isClassMethod() || parent.isTSPropertySignature();
583
+ if (keyed && parent.node.key === path.node && !parent.node.computed)
584
+ return true;
585
+ return !!path.findParent((p) => p.isTSType() || p.isTSTypeAnnotation() || p.isTSEnumMember());
586
+ }
587
+ /**
588
+ * The expression a literal is the value of: through `?:` branches, `??`/`||`/`&&`,
589
+ * parentheses and type assertions (`'image-outline' as const`).
590
+ */
591
+ function valueSite(path) {
592
+ let site = path;
593
+ for (;;) {
594
+ const parent = site.parentPath;
595
+ if (!parent)
596
+ return site;
597
+ const passesThrough = (parent.isConditionalExpression() && site.key !== 'test') ||
598
+ parent.isLogicalExpression() ||
599
+ parent.isParenthesizedExpression() ||
600
+ parent.isTSAsExpression() ||
601
+ parent.isTSSatisfiesExpression() ||
602
+ parent.isTSNonNullExpression();
603
+ if (!passesThrough)
604
+ return site;
605
+ site = parent;
606
+ }
607
+ }
551
608
  /**
552
609
  * Extract strings from a Vue Single File Component (SFC).
553
610
  * Splits the SFC into template and script sections, extracts strings
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Package versions finalization adds to a project's package.json.
3
+ *
4
+ * i18next 26 / react-i18next 17 are the first majors whose optional
5
+ * TypeScript peer accepts TypeScript 6 — the default in new Vite and Expo
6
+ * apps. Older pins made `npm install` fail with ERESOLVE on the PR.
7
+ */
8
+ export declare const I18NEXT_CORE_DEPENDENCIES: Record<string, string>;
9
+ /** Browser apps load /locales/<lng>.json over HTTP and detect the browser's language. */
10
+ export declare const I18NEXT_WEB_DEPENDENCIES: Record<string, string>;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.I18NEXT_WEB_DEPENDENCIES = exports.I18NEXT_CORE_DEPENDENCIES = void 0;
4
+ /**
5
+ * Package versions finalization adds to a project's package.json.
6
+ *
7
+ * i18next 26 / react-i18next 17 are the first majors whose optional
8
+ * TypeScript peer accepts TypeScript 6 — the default in new Vite and Expo
9
+ * apps. Older pins made `npm install` fail with ERESOLVE on the PR.
10
+ */
11
+ exports.I18NEXT_CORE_DEPENDENCIES = {
12
+ 'i18next': '^26.4.0',
13
+ 'react-i18next': '^17.0.0',
14
+ // Messages are ICU ("Welcome back, {user}", plurals): i18next-icu formats them.
15
+ 'i18next-icu': '^2.5.0',
16
+ 'intl-messageformat': '^10.7.0',
17
+ };
18
+ /** Browser apps load /locales/<lng>.json over HTTP and detect the browser's language. */
19
+ exports.I18NEXT_WEB_DEPENDENCIES = {
20
+ ...exports.I18NEXT_CORE_DEPENDENCIES,
21
+ 'i18next-http-backend': '^4.0.0',
22
+ 'i18next-browser-languagedetector': '^8.2.0',
23
+ };
@@ -45,43 +45,22 @@ const static_runtime_1 = require("./static-runtime");
45
45
  const django_finalize_1 = require("./django-finalize");
46
46
  const nextjs_finalize_1 = require("./nextjs-finalize");
47
47
  const writes_1 = require("./writes");
48
+ const react_native_finalize_1 = require("./react-native-finalize");
49
+ const dependencies_1 = require("./dependencies");
50
+ const i18n_config_path_1 = require("../../utils/i18n-config-path");
48
51
  const i18next_icu_1 = require("../../utils/i18next-icu");
49
52
  const DEPENDENCIES_BY_FRAMEWORK = {
50
- react: {
51
- 'i18next': '^24.0.0',
52
- 'react-i18next': '^14.0.0',
53
- 'i18next-http-backend': '^2.0.2',
54
- 'i18next-browser-languagedetector': '^8.0.0',
55
- // Messages are ICU ("Welcome back, {user}", plurals): i18next-icu formats them.
56
- 'i18next-icu': '^2.5.0',
57
- 'intl-messageformat': '^10.7.0',
58
- },
53
+ react: dependencies_1.I18NEXT_WEB_DEPENDENCIES,
59
54
  // Next.js projects that already use react-i18next keep it (injection picks
60
55
  // ReactI18nextStrategy for them). Everything else gets next-intl — see
61
56
  // NEXT_INTL_DEPENDENCIES and finalizeNextIntl().
62
- nextjs: {
63
- 'i18next': '^24.0.0',
64
- 'react-i18next': '^14.0.0',
65
- 'i18next-http-backend': '^2.0.2',
66
- 'i18next-browser-languagedetector': '^8.0.0',
67
- // Messages are ICU ("Welcome back, {user}", plurals): i18next-icu formats them.
68
- 'i18next-icu': '^2.5.0',
69
- 'intl-messageformat': '^10.7.0',
70
- },
57
+ nextjs: dependencies_1.I18NEXT_WEB_DEPENDENCIES,
71
58
  vue: {
72
59
  'vue-i18n': '^10.0.0',
73
60
  },
74
61
  static: {},
75
62
  django: {},
76
- unknown: {
77
- 'i18next': '^24.0.0',
78
- 'react-i18next': '^14.0.0',
79
- 'i18next-http-backend': '^2.0.2',
80
- 'i18next-browser-languagedetector': '^8.0.0',
81
- // Messages are ICU ("Welcome back, {user}", plurals): i18next-icu formats them.
82
- 'i18next-icu': '^2.5.0',
83
- 'intl-messageformat': '^10.7.0',
84
- },
63
+ unknown: dependencies_1.I18NEXT_WEB_DEPENDENCIES,
85
64
  };
86
65
  const I18N_CONFIG_TEMPLATES = {
87
66
  react: `${i18next_icu_1.GENERATED_I18N_CONFIG_MARKER}
@@ -101,7 +80,8 @@ i18n
101
80
  .init({
102
81
  fallbackLng: 'en',
103
82
  defaultNS: 'translation',
104
- debug: process.env.NODE_ENV !== 'production',
83
+ // No process.env here: Vite and other browser builds don't define it (or its types).
84
+ debug: false,
105
85
  interpolation: {
106
86
  escapeValue: false,
107
87
  },
@@ -198,7 +178,7 @@ i18n
198
178
  .init({
199
179
  fallbackLng: 'en',
200
180
  defaultNS: 'translation',
201
- debug: process.env.NODE_ENV !== 'production',
181
+ debug: false,
202
182
  interpolation: {
203
183
  escapeValue: false,
204
184
  },
@@ -250,12 +230,15 @@ async function finalizeProject(workspace, dryRun, options = {}) {
250
230
  const framework = types_1.FRAMEWORKS.includes(workspace.framework)
251
231
  ? workspace.framework
252
232
  : 'unknown';
253
- let requiredDeps = DEPENDENCIES_BY_FRAMEWORK[framework];
233
+ let requiredDeps = DEPENDENCIES_BY_FRAMEWORK[framework] ?? {};
254
234
  // Mirrors StrategyFactory: a Next.js project that already has react-i18next
255
235
  // is injected with react-i18next; every other Next.js project gets next-intl.
256
236
  let useNextIntl = framework === 'nextjs';
257
237
  if (!['static', 'django'].includes(framework)) {
258
- const packageJsonPath = path.join(workspace.path, 'package.json');
238
+ // The app's package.json (apps/mobile in a monorepo); the install below
239
+ // still runs at the root, where workspaces install and the lockfile lives.
240
+ const appRoot = path.join(workspace.path, workspace.appDir ?? '.');
241
+ const packageJsonPath = path.join(appRoot, 'package.json');
259
242
  if (await (0, file_utils_1.pathExists)(packageJsonPath)) {
260
243
  const pkg = await (0, file_utils_1.readJsonFile)(packageJsonPath);
261
244
  if (pkg) {
@@ -264,6 +247,13 @@ async function finalizeProject(workspace, dryRun, options = {}) {
264
247
  useNextIntl = false;
265
248
  if (useNextIntl)
266
249
  requiredDeps = nextjs_finalize_1.NEXT_INTL_DEPENDENCIES;
250
+ if (framework === 'react-native') {
251
+ // expo-localization pinned to the app's Expo SDK, or react-native-localize.
252
+ const rn = (0, react_native_finalize_1.reactNativeDependencies)(pkg);
253
+ requiredDeps = rn.deps;
254
+ if (rn.warning)
255
+ warnings.push(rn.warning);
256
+ }
267
257
  const missingDeps = Object.keys(requiredDeps).filter((d) => !deps[d]);
268
258
  if (missingDeps.length > 0) {
269
259
  pkg.dependencies = pkg.dependencies || {};
@@ -334,7 +324,7 @@ async function finalizeProject(workspace, dryRun, options = {}) {
334
324
  }
335
325
  }
336
326
  }
337
- const srcDir = workspace.sourceDirs.find((d) => d.includes('src')) || workspace.sourceDirs[0] || '.';
327
+ const srcDir = (0, i18n_config_path_1.i18nConfigDir)(workspace);
338
328
  if (framework === 'static') {
339
329
  const i18nJsPath = path.join(workspace.path, 'i18n.js');
340
330
  const existingContent = await (0, file_utils_1.pathExists)(i18nJsPath) ? await (0, file_utils_1.readFile)(i18nJsPath) : null;
@@ -379,6 +369,16 @@ async function finalizeProject(workspace, dryRun, options = {}) {
379
369
  }
380
370
  }
381
371
  }
372
+ else if (framework === 'react-native') {
373
+ const rn = await (0, react_native_finalize_1.finalizeReactNative)(workspace, {
374
+ sourceLocale: options.sourceLocale,
375
+ targetLocales: options.targetLocales,
376
+ }, writer);
377
+ warnings.push(...rn.warnings);
378
+ userOwnsInit = rn.userOwnsInit;
379
+ configFileCreated = configFileCreated || rn.configFileCreated;
380
+ entryPointModified = entryPointModified || rn.entryPointModified;
381
+ }
382
382
  else if (useNextIntl) {
383
383
  const nextIntl = await (0, nextjs_finalize_1.finalizeNextIntl)(workspace, dryRun, {
384
384
  sourceLocale: options.sourceLocale,
@@ -412,7 +412,7 @@ async function finalizeProject(workspace, dryRun, options = {}) {
412
412
  userOwnsInit = icu.userOwnsInit;
413
413
  if (!userOwnsInit && !(await (0, file_utils_1.pathExists)(i18nPath)) && !(await (0, file_utils_1.pathExists)(i18nJsPath))) {
414
414
  const isTS = workspace.sourceDirs.some((d) => d.endsWith('ts') || d.endsWith('tsx')) ||
415
- (await (0, file_utils_1.pathExists)(path.join(workspace.path, 'tsconfig.json')));
415
+ (await (0, file_utils_1.pathExists)(path.join(workspace.path, workspace.appDir ?? '.', 'tsconfig.json')));
416
416
  const targetPath = isTS ? i18nPath : i18nJsPath;
417
417
  const template = I18N_CONFIG_TEMPLATES[framework] || I18N_CONFIG_TEMPLATES['unknown'];
418
418
  await writer.write(targetPath, template);
@@ -450,7 +450,7 @@ async function finalizeProject(workspace, dryRun, options = {}) {
450
450
  ];
451
451
  if (framework === 'vue') {
452
452
  for (const entry of entryPoints) {
453
- const entryPath = path.join(workspace.path, entry);
453
+ const entryPath = path.join(workspace.path, workspace.appDir ?? '.', entry);
454
454
  if (await (0, file_utils_1.pathExists)(entryPath)) {
455
455
  const content = await (0, file_utils_1.readFile)(entryPath);
456
456
  const entryDir = path.dirname(entryPath);
@@ -468,9 +468,9 @@ async function finalizeProject(workspace, dryRun, options = {}) {
468
468
  }
469
469
  }
470
470
  }
471
- else if (!['static', 'django'].includes(framework) && !useNextIntl && !userOwnsInit) {
471
+ else if (!['static', 'django', 'react-native'].includes(framework) && !useNextIntl && !userOwnsInit) {
472
472
  for (const entry of entryPoints) {
473
- const entryPath = path.join(workspace.path, entry);
473
+ const entryPath = path.join(workspace.path, workspace.appDir ?? '.', entry);
474
474
  if (await (0, file_utils_1.pathExists)(entryPath)) {
475
475
  const content = await (0, file_utils_1.readFile)(entryPath);
476
476
  const entryDir = path.dirname(entryPath);
@@ -515,7 +515,7 @@ function modifyNextjsLayout(content, relativeI18nPath) {
515
515
  const providerImportPath = relativeI18nPath.replace(/i18n(\.(ts|js))?$/, 'I18nProvider');
516
516
  const isTS = content.includes(': Readonly') || content.includes(': React.ReactNode');
517
517
  try {
518
- const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript: isTS });
518
+ const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript: isTS, preserveFormat: true });
519
519
  let modified = false;
520
520
  let wrapped = false;
521
521
  const sideEffectImports = [
@@ -596,7 +596,7 @@ function modifyNextjsLayoutFallback(content, providerImportPath) {
596
596
  function modifyVueEntryPoint(content, relativeI18nPath) {
597
597
  const isTS = /\.tsx?$/.test(relativeI18nPath) || /\bfrom\s+['"]vue['"]/.test(content) || /:\s*(?:string|number|boolean|Ref|ComputedRef)/.test(content);
598
598
  try {
599
- const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript: isTS });
599
+ const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript: isTS, preserveFormat: true });
600
600
  let hasExistingImport = false;
601
601
  let modified = false;
602
602
  for (const node of ast.program.body) {
@@ -739,7 +739,9 @@ function i18nRuntimeFor(framework, pkg) {
739
739
  return { library: 'Django gettext', required: [], missing: [] };
740
740
  const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
741
741
  const nextIntl = fw === 'nextjs' && !deps['react-i18next'];
742
- const required = Object.keys(nextIntl ? nextjs_finalize_1.NEXT_INTL_DEPENDENCIES : DEPENDENCIES_BY_FRAMEWORK[fw]);
742
+ const required = Object.keys(nextIntl ? nextjs_finalize_1.NEXT_INTL_DEPENDENCIES
743
+ : fw === 'react-native' ? (0, react_native_finalize_1.reactNativeDependencies)(pkg ?? undefined).deps
744
+ : DEPENDENCIES_BY_FRAMEWORK[fw] ?? {});
743
745
  const library = nextIntl ? 'next-intl' : fw === 'vue' ? 'vue-i18n' : 'react-i18next';
744
746
  return { library, required, missing: required.filter((d) => !(pkg?.dependencies ?? {})[d]) };
745
747
  }
@@ -241,7 +241,8 @@ writer = new writes_1.FinalizationWriter(workspace.path, dryRun)) {
241
241
  const warnings = [];
242
242
  let configFileCreated = false;
243
243
  let entryPointModified = false;
244
- const root = workspace.path;
244
+ // The app's folder: the repo root, or apps/web in a monorepo.
245
+ const root = path.join(workspace.path, workspace.appDir ?? '.');
245
246
  const typescript = await (0, file_utils_1.pathExists)(path.join(root, 'tsconfig.json'));
246
247
  const appDir = (await (0, file_utils_1.pathExists)(path.join(root, 'app')))
247
248
  ? path.join(root, 'app')