@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.
- 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/strategies/VanillaStrategy.d.ts +8 -0
- package/dist/injection/strategies/VanillaStrategy.js +10 -3
- package/dist/local/translate-core.d.ts +1 -1
- package/dist/local/translate-core.js +1 -1
- 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 +41 -39
- 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/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/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
|
}
|
|
@@ -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
|
|
@@ -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.
|
|
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
|
+
};
|
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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')
|