@babelize/engine 0.1.3 → 0.2.1

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 (55) hide show
  1. package/dist/analysis/analyze.js +1 -1
  2. package/dist/config.d.ts +3 -3
  3. package/dist/detection/stack.d.ts +3 -1
  4. package/dist/detection/stack.js +13 -6
  5. package/dist/injection/StrategyFactory.d.ts +5 -1
  6. package/dist/injection/StrategyFactory.js +7 -2
  7. package/dist/injection/html/html-injector.d.ts +2 -0
  8. package/dist/injection/html/html-injector.js +55 -6
  9. package/dist/injection/strategies/VanillaStrategy.d.ts +8 -0
  10. package/dist/injection/strategies/VanillaStrategy.js +10 -3
  11. package/dist/local/translate-core.d.ts +2 -2
  12. package/dist/local/translate-core.js +2 -2
  13. package/dist/modules/extractor/extract.js +67 -10
  14. package/dist/modules/finalization/dependencies.d.ts +10 -0
  15. package/dist/modules/finalization/dependencies.js +23 -0
  16. package/dist/modules/finalization/finalize.js +47 -43
  17. package/dist/modules/finalization/nextjs-finalize.js +2 -1
  18. package/dist/modules/finalization/react-native-finalize.d.ts +45 -0
  19. package/dist/modules/finalization/react-native-finalize.js +324 -0
  20. package/dist/modules/finalization/static-runtime.d.ts +2 -2
  21. package/dist/modules/finalization/static-runtime.js +10 -5
  22. package/dist/modules/format-injection/index.js +38 -9
  23. package/dist/modules/format-injection/inject-formats.d.ts +3 -2
  24. package/dist/modules/format-injection/inject-formats.js +4 -3
  25. package/dist/modules/injection/inject.d.ts +14 -1
  26. package/dist/modules/injection/inject.js +307 -154
  27. package/dist/modules/sync-detector/sync-detector.js +11 -4
  28. package/dist/modules/translator/line-breaks.d.ts +8 -0
  29. package/dist/modules/translator/line-breaks.js +63 -0
  30. package/dist/modules/translator/translate.js +5 -2
  31. package/dist/modules/workspace/validate.d.ts +13 -1
  32. package/dist/modules/workspace/validate.js +95 -58
  33. package/dist/runtime/types.d.ts +22 -0
  34. package/dist/runtime/types.js +33 -0
  35. package/dist/utils/ast-helpers.d.ts +30 -2
  36. package/dist/utils/ast-helpers.js +355 -42
  37. package/dist/utils/extraction-options.js +1 -1
  38. package/dist/utils/file-utils.js +4 -1
  39. package/dist/utils/i18n-config-path.d.ts +18 -0
  40. package/dist/utils/i18n-config-path.js +64 -0
  41. package/dist/utils/i18next-icu.js +1 -1
  42. package/dist/utils/jsx-message.d.ts +16 -1
  43. package/dist/utils/jsx-message.js +89 -9
  44. package/dist/utils/key-usage.js +1 -1
  45. package/dist/utils/metadata.d.ts +5 -1
  46. package/dist/utils/metadata.js +19 -1
  47. package/dist/utils/module-constants.d.ts +21 -3
  48. package/dist/utils/module-constants.js +123 -25
  49. package/dist/utils/react-native.d.ts +17 -0
  50. package/dist/utils/react-native.js +108 -0
  51. package/dist/utils/string-utils.d.ts +8 -0
  52. package/dist/utils/string-utils.js +29 -1
  53. package/dist/utils/ui-copy.d.ts +5 -4
  54. package/dist/utils/ui-copy.js +10 -5
  55. package/package.json +3 -2
@@ -139,7 +139,7 @@ async function analyzeWorkspace(workspacePath, options = {}) {
139
139
  const framework = workspace.framework;
140
140
  const support = frameworkSupport(framework);
141
141
  if (support === 'unsupported') {
142
- return empty(framework, `Detected ${framework}, which local translation does not support. Supported: React, Next.js and Django (Vue and static HTML in beta).`);
142
+ return empty(framework, `Detected ${framework}, which local translation does not support. Supported: React, Next.js, Django and static HTML (Vue and React Native in beta).`);
143
143
  }
144
144
  const phases = (types_1.FRAMEWORK_CONFIGS[framework] ?? types_1.FRAMEWORK_CONFIGS.unknown).phases;
145
145
  // Same ICU decision as a local translate run (whose translator is an LLM).
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
  }
@@ -7,5 +7,7 @@ interface HtmlInjectOptions {
7
7
  targetLocales?: string[];
8
8
  sourceLocale?: string;
9
9
  }
10
+ /** `<script src>` for the root i18n.js as seen from this page: `i18n.js`, `../i18n.js`, ... */
11
+ export declare function runtimeScriptSrc(filePath: string): string;
10
12
  export declare function injectHtml(options: HtmlInjectOptions): Promise<FileInjection>;
11
13
  export {};
@@ -32,10 +32,36 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.runtimeScriptSrc = runtimeScriptSrc;
36
40
  exports.injectHtml = injectHtml;
41
+ const path_1 = __importDefault(require("path"));
37
42
  const string_utils_1 = require("../../utils/string-utils");
38
43
  const html_extractor_1 = require("../../utils/html-extractor");
44
+ // <meta> tags whose content the extractor collects (html-extractor extractHeadStrings).
45
+ const SEO_META = ['description', 'og:title', 'og:description', 'og:site_name', 'twitter:title', 'twitter:description'];
46
+ /**
47
+ * Key for a string in this file found by its text alone. Head strings are extracted
48
+ * with ids like `file:meta:<hash>`, and a string already seen in the body is not
49
+ * extracted twice, so a line-based id can't find them.
50
+ */
51
+ function keyByText(filePath, text, keyMapping) {
52
+ const prefix = `${filePath}:`;
53
+ const suffix = `:${(0, string_utils_1.hashString)(text)}`;
54
+ for (const [id, key] of keyMapping) {
55
+ if (id.startsWith(prefix) && id.endsWith(suffix))
56
+ return key;
57
+ }
58
+ return undefined;
59
+ }
60
+ /** `<script src>` for the root i18n.js as seen from this page: `i18n.js`, `../i18n.js`, ... */
61
+ function runtimeScriptSrc(filePath) {
62
+ const dir = path_1.default.posix.dirname(filePath.split(path_1.default.sep).join('/'));
63
+ return path_1.default.posix.relative(dir, 'i18n.js') || 'i18n.js';
64
+ }
39
65
  function asElement(node) {
40
66
  return node.nodeType === 1 ? node : null;
41
67
  }
@@ -60,13 +86,26 @@ function walkHtmlNode(node, filePath, content, keyMapping, matchedIds, injection
60
86
  }
61
87
  }
62
88
  const element = asElement(node);
89
+ if (element && tagName === 'meta') {
90
+ const name = element.getAttribute('name') || element.getAttribute('property');
91
+ const value = element.getAttribute('content');
92
+ if (name && SEO_META.includes(name) && value?.trim() && !element.getAttribute('data-i18n-content')) {
93
+ const key = keyByText(filePath, value.trim(), keyMapping);
94
+ if (key) {
95
+ element.setAttribute('data-i18n-content', key);
96
+ injections.push({ file: filePath, line: 0, column: 0, original: value, replacement: `data-i18n-content="${key}"`, key, confidence: 100 });
97
+ }
98
+ }
99
+ return;
100
+ }
63
101
  if (textChild && !nonTextChild && element && !element.getAttribute('data-i18n')) {
64
102
  const text = textChild.text.trim();
65
103
  if (node.range) {
66
104
  const line = (content.slice(0, node.range[0]).match(/\n/g) || []).length + 1;
67
105
  const id = `${filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
68
- if (matchedIds.has(id)) {
69
- const key = keyMapping.get(id);
106
+ // <title> may only exist under a head id (`file:title:<hash>`) or share a body string's key.
107
+ if (matchedIds.has(id) || tagName === 'title') {
108
+ const key = keyMapping.get(id) ?? (tagName === 'title' ? keyByText(filePath, text, keyMapping) : undefined);
70
109
  if (key) {
71
110
  element.setAttribute('data-i18n', key);
72
111
  injections.push({
@@ -159,13 +198,23 @@ async function injectHtml(options) {
159
198
  const matchedIds = new Set(keyMapping.keys());
160
199
  walkHtmlNode(htmlEl, filePath, content, keyMapping, matchedIds, injections);
161
200
  let modifiedContent = root.toString();
162
- const hasScript = /<script[^>]*src\s*=\s*["']i18n\.js["'][^>]*>/.test(modifiedContent);
163
- if (!hasScript) {
201
+ // i18n.js lives at the site root, so pages in subfolders need ../i18n.js. Older runs
202
+ // wrote a bare src="i18n.js" everywhere; repoint those instead of adding a second tag.
203
+ const src = runtimeScriptSrc(filePath);
204
+ const existingScript = /<script([^>]*)src\s*=\s*["']((?:\.\.\/)*i18n\.js)["']/.exec(modifiedContent);
205
+ if (existingScript) {
206
+ if (existingScript[2] !== src) {
207
+ modifiedContent = modifiedContent.replace(existingScript[0], `<script${existingScript[1]}src="${src}"`);
208
+ importAdded = true;
209
+ }
210
+ }
211
+ else {
212
+ const tag = ` <script src="${src}"></script>\n`;
164
213
  if (modifiedContent.includes('</body>')) {
165
- modifiedContent = modifiedContent.replace('</body>', ' <script src="i18n.js"></script>\n</body>');
214
+ modifiedContent = modifiedContent.replace('</body>', `${tag}</body>`);
166
215
  }
167
216
  else {
168
- modifiedContent = modifiedContent.replace('</head>', ' <script src="i18n.js"></script>\n</head>');
217
+ modifiedContent = modifiedContent.replace('</head>', `${tag}</head>`);
169
218
  }
170
219
  importAdded = true;
171
220
  }
@@ -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
@@ -10,8 +10,8 @@ import { type FileDiffStat, type ApplyIo, type WorkspaceChanges } from './worksp
10
10
  /** Lockfile scope for local runs: `.babelize/local/babelize-lock.json` in the project root. */
11
11
  export declare const LOCAL_PROJECT_ID = "local";
12
12
  export declare const LOCAL_LOCKFILE = ".babelize/local/babelize-lock.json";
13
- export declare const SUPPORTED_LOCAL_FRAMEWORKS: readonly ["react", "nextjs", "django"];
14
- export declare const BETA_LOCAL_FRAMEWORKS: readonly ["vue", "static"];
13
+ export declare const SUPPORTED_LOCAL_FRAMEWORKS: readonly ["react", "nextjs", "django", "static"];
14
+ export declare const BETA_LOCAL_FRAMEWORKS: readonly ["vue", "react-native"];
15
15
  export interface LocalTranslateOptions {
16
16
  projectDir: string;
17
17
  sourceLocale: string;
@@ -72,8 +72,8 @@ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
72
72
  /** Lockfile scope for local runs: `.babelize/local/babelize-lock.json` in the project root. */
73
73
  exports.LOCAL_PROJECT_ID = 'local';
74
74
  exports.LOCAL_LOCKFILE = `.babelize/${exports.LOCAL_PROJECT_ID}/babelize-lock.json`;
75
- exports.SUPPORTED_LOCAL_FRAMEWORKS = ['react', 'nextjs', 'django'];
76
- exports.BETA_LOCAL_FRAMEWORKS = ['vue', 'static'];
75
+ exports.SUPPORTED_LOCAL_FRAMEWORKS = ['react', 'nextjs', 'django', 'static'];
76
+ exports.BETA_LOCAL_FRAMEWORKS = ['vue', '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
+ };