@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.
- package/dist/analysis/analyze.js +1 -1
- package/dist/config.d.ts +3 -3
- package/dist/detection/stack.d.ts +3 -1
- package/dist/detection/stack.js +13 -6
- package/dist/injection/StrategyFactory.d.ts +5 -1
- package/dist/injection/StrategyFactory.js +7 -2
- package/dist/injection/html/html-injector.d.ts +2 -0
- package/dist/injection/html/html-injector.js +55 -6
- package/dist/injection/strategies/VanillaStrategy.d.ts +8 -0
- package/dist/injection/strategies/VanillaStrategy.js +10 -3
- package/dist/local/translate-core.d.ts +2 -2
- package/dist/local/translate-core.js +2 -2
- package/dist/modules/extractor/extract.js +67 -10
- package/dist/modules/finalization/dependencies.d.ts +10 -0
- package/dist/modules/finalization/dependencies.js +23 -0
- package/dist/modules/finalization/finalize.js +47 -43
- package/dist/modules/finalization/nextjs-finalize.js +2 -1
- package/dist/modules/finalization/react-native-finalize.d.ts +45 -0
- package/dist/modules/finalization/react-native-finalize.js +324 -0
- package/dist/modules/finalization/static-runtime.d.ts +2 -2
- package/dist/modules/finalization/static-runtime.js +10 -5
- package/dist/modules/format-injection/index.js +38 -9
- package/dist/modules/format-injection/inject-formats.d.ts +3 -2
- package/dist/modules/format-injection/inject-formats.js +4 -3
- package/dist/modules/injection/inject.d.ts +14 -1
- package/dist/modules/injection/inject.js +307 -154
- package/dist/modules/sync-detector/sync-detector.js +11 -4
- package/dist/modules/translator/line-breaks.d.ts +8 -0
- package/dist/modules/translator/line-breaks.js +63 -0
- package/dist/modules/translator/translate.js +5 -2
- package/dist/modules/workspace/validate.d.ts +13 -1
- package/dist/modules/workspace/validate.js +95 -58
- package/dist/runtime/types.d.ts +22 -0
- package/dist/runtime/types.js +33 -0
- package/dist/utils/ast-helpers.d.ts +30 -2
- package/dist/utils/ast-helpers.js +355 -42
- package/dist/utils/extraction-options.js +1 -1
- package/dist/utils/file-utils.js +4 -1
- package/dist/utils/i18n-config-path.d.ts +18 -0
- package/dist/utils/i18n-config-path.js +64 -0
- package/dist/utils/i18next-icu.js +1 -1
- package/dist/utils/jsx-message.d.ts +16 -1
- package/dist/utils/jsx-message.js +89 -9
- package/dist/utils/key-usage.js +1 -1
- package/dist/utils/metadata.d.ts +5 -1
- package/dist/utils/metadata.js +19 -1
- package/dist/utils/module-constants.d.ts +21 -3
- package/dist/utils/module-constants.js +123 -25
- package/dist/utils/react-native.d.ts +17 -0
- package/dist/utils/react-native.js +108 -0
- package/dist/utils/string-utils.d.ts +8 -0
- package/dist/utils/string-utils.js +29 -1
- package/dist/utils/ui-copy.d.ts +5 -4
- package/dist/utils/ui-copy.js +10 -5
- package/package.json +3 -2
package/dist/analysis/analyze.js
CHANGED
|
@@ -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
|
|
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. */
|
package/dist/detection/stack.js
CHANGED
|
@@ -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',
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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>',
|
|
214
|
+
modifiedContent = modifiedContent.replace('</body>', `${tag}</body>`);
|
|
166
215
|
}
|
|
167
216
|
else {
|
|
168
|
-
modifiedContent = modifiedContent.replace('</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 ===
|
|
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(
|
|
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", "
|
|
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', '
|
|
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.
|
|
68
|
-
//
|
|
69
|
-
|
|
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 =
|
|
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
|
-
|
|
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)(
|
|
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) &&
|
|
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
|
-
|
|
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
|
+
};
|