@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
|
@@ -40,7 +40,9 @@ const logger_1 = require("../../utils/logger");
|
|
|
40
40
|
const extract_1 = require("../extractor/extract");
|
|
41
41
|
const lockfile_manager_1 = require("../lockfile/lockfile-manager");
|
|
42
42
|
const types_1 = require("../../runtime/types");
|
|
43
|
+
const types_2 = require("@babelize/types");
|
|
43
44
|
const extraction_options_1 = require("../../utils/extraction-options");
|
|
45
|
+
const validate_1 = require("../workspace/validate");
|
|
44
46
|
/**
|
|
45
47
|
* Extraction options the project's engine runs use (sentence grouping, ICU
|
|
46
48
|
* plurals, data-literal rules), so a sync check compares like with like and
|
|
@@ -55,7 +57,8 @@ async function runExtractionOptions(workspacePath, options) {
|
|
|
55
57
|
packageJson = undefined;
|
|
56
58
|
}
|
|
57
59
|
const deps = { ...packageJson?.dependencies, ...packageJson?.devDependencies };
|
|
58
|
-
const framework = options.framework ??
|
|
60
|
+
const framework = options.framework ??
|
|
61
|
+
(deps.next ? 'nextjs' : (0, validate_1.isReactNativePackage)(packageJson) ? 'react-native' : deps.react ? 'react' : deps.vue ? 'vue' : 'unknown');
|
|
59
62
|
const workspace = {
|
|
60
63
|
path: workspacePath, framework, sourceDirs: ['.'], hasLocalesDir: false, existingLocales: [], publicDir: 'public',
|
|
61
64
|
localeFormat: 'json', packageJson,
|
|
@@ -112,10 +115,13 @@ const SYNC_IGNORE_PATTERNS = [
|
|
|
112
115
|
* Check if a file path matches supported source extensions
|
|
113
116
|
* and is not excluded by ignore patterns.
|
|
114
117
|
*/
|
|
115
|
-
function isSourceFile(filePath) {
|
|
118
|
+
function isSourceFile(filePath, frameworkExcludes = []) {
|
|
116
119
|
const ext = path.extname(filePath).toLowerCase();
|
|
117
120
|
if (!types_1.SUPPORTED_EXTENSIONS.includes(ext))
|
|
118
121
|
return false;
|
|
122
|
+
// What runs never scan for this framework (React Native's ios/android projects, scripts/).
|
|
123
|
+
if ((0, types_1.isExcludedPath)(filePath, frameworkExcludes))
|
|
124
|
+
return false;
|
|
119
125
|
for (const pattern of SYNC_IGNORE_PATTERNS) {
|
|
120
126
|
if (pattern.test(filePath))
|
|
121
127
|
return false;
|
|
@@ -140,9 +146,11 @@ async function detectSyncIssues(projectId, workspacePath, changedFiles, commitSh
|
|
|
140
146
|
const filesChecked = [];
|
|
141
147
|
const filesSkipped = [];
|
|
142
148
|
// 1. Filter changed files to only source files
|
|
149
|
+
const extractOptions = await runExtractionOptions(workspacePath, options);
|
|
150
|
+
const frameworkExcludes = types_2.FRAMEWORK_CONFIGS[extractOptions.framework]?.excludedPatterns ?? [];
|
|
143
151
|
const sourceFiles = [];
|
|
144
152
|
for (const file of changedFiles) {
|
|
145
|
-
if (isSourceFile(file)) {
|
|
153
|
+
if (isSourceFile(file, frameworkExcludes)) {
|
|
146
154
|
sourceFiles.push(file);
|
|
147
155
|
}
|
|
148
156
|
else {
|
|
@@ -168,7 +176,6 @@ async function detectSyncIssues(projectId, workspacePath, changedFiles, commitSh
|
|
|
168
176
|
}
|
|
169
177
|
// 2. Extract strings from changed source files, the way runs do
|
|
170
178
|
const extractedStrings = {};
|
|
171
|
-
const extractOptions = await runExtractionOptions(workspacePath, options);
|
|
172
179
|
for (const relativePath of sourceFiles) {
|
|
173
180
|
const absolutePath = path.join(workspacePath, relativePath);
|
|
174
181
|
try {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TranslationProvider } from '../../providers/types';
|
|
2
|
+
/**
|
|
3
|
+
* A plain-text translator (NLLB) works sentence by sentence and drops line
|
|
4
|
+
* breaks: "…is running.\n\nDetails: {x}" came back as one line. Messages with
|
|
5
|
+
* line breaks go line by line, and are put back together with the source's
|
|
6
|
+
* breaks before the usual checks.
|
|
7
|
+
*/
|
|
8
|
+
export declare function translateLineByLine(provider: TranslationProvider): TranslationProvider;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.translateLineByLine = translateLineByLine;
|
|
4
|
+
const BREAK = /([ \t]*\r?\n[\s]*)/;
|
|
5
|
+
/** The translator service's batch limit (apps/translator `max_batch_size`). */
|
|
6
|
+
const MAX_BATCH = 50;
|
|
7
|
+
/**
|
|
8
|
+
* A plain-text translator (NLLB) works sentence by sentence and drops line
|
|
9
|
+
* breaks: "…is running.\n\nDetails: {x}" came back as one line. Messages with
|
|
10
|
+
* line breaks go line by line, and are put back together with the source's
|
|
11
|
+
* breaks before the usual checks.
|
|
12
|
+
*/
|
|
13
|
+
function translateLineByLine(provider) {
|
|
14
|
+
return {
|
|
15
|
+
pseudo: provider.pseudo,
|
|
16
|
+
plainText: provider.plainText,
|
|
17
|
+
async translateBatch(request, options) {
|
|
18
|
+
const split = new Map();
|
|
19
|
+
const entries = [];
|
|
20
|
+
for (const entry of request.entries) {
|
|
21
|
+
const parts = entry.value.split(BREAK);
|
|
22
|
+
if (parts.length === 1) {
|
|
23
|
+
entries.push(entry);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
split.set(entry.key, parts);
|
|
27
|
+
// Odd indexes are the breaks; empty lines have nothing to translate.
|
|
28
|
+
parts.forEach((part, i) => {
|
|
29
|
+
if (i % 2 === 0 && part.trim())
|
|
30
|
+
entries.push({ ...entry, key: `${entry.key}#line${i}`, value: part });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (split.size === 0)
|
|
34
|
+
return provider.translateBatch(request, options);
|
|
35
|
+
// Lines add entries: keep each request within the translator's batch limit.
|
|
36
|
+
const results = new Map();
|
|
37
|
+
for (let i = 0; i < entries.length; i += MAX_BATCH) {
|
|
38
|
+
const part = await provider.translateBatch({ ...request, entries: entries.slice(i, i + MAX_BATCH) }, options);
|
|
39
|
+
for (const r of part)
|
|
40
|
+
results.set(r.key, r);
|
|
41
|
+
}
|
|
42
|
+
return request.entries.map((entry) => {
|
|
43
|
+
const parts = split.get(entry.key);
|
|
44
|
+
if (!parts)
|
|
45
|
+
return results.get(entry.key) ?? { key: entry.key, status: 'failed', reason: 'missing_from_response', retryable: true };
|
|
46
|
+
let value = '';
|
|
47
|
+
for (let i = 0; i < parts.length; i++) {
|
|
48
|
+
if (i % 2 === 1 || !parts[i].trim()) {
|
|
49
|
+
value += parts[i];
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const line = results.get(`${entry.key}#line${i}`);
|
|
53
|
+
if (!line)
|
|
54
|
+
return { key: entry.key, status: 'failed', reason: 'missing_from_response', retryable: true };
|
|
55
|
+
if (line.status !== 'ok')
|
|
56
|
+
return { ...line, key: entry.key };
|
|
57
|
+
value += line.value;
|
|
58
|
+
}
|
|
59
|
+
return { key: entry.key, status: 'ok', value };
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -9,6 +9,7 @@ const untranslated_1 = require("../../utils/untranslated");
|
|
|
9
9
|
const logger_1 = require("../../utils/logger");
|
|
10
10
|
const translation_quality_1 = require("../../utils/translation-quality");
|
|
11
11
|
const file_utils_1 = require("../../utils/file-utils");
|
|
12
|
+
const line_breaks_1 = require("./line-breaks");
|
|
12
13
|
const placeholder_protection_1 = require("../../utils/placeholder-protection");
|
|
13
14
|
const icu_message_1 = require("../../utils/icu-message");
|
|
14
15
|
const keygen_1 = require("../keygen");
|
|
@@ -549,6 +550,8 @@ async function translateEntries(entries, sourceLang, targetLang, projectId, onPr
|
|
|
549
550
|
// service translates plain text only: ICU messages go branch by branch
|
|
550
551
|
// (./icu-branches), and mixed-script text fails up front.
|
|
551
552
|
const isLlm = !!byokConfig || (!!providers.translator && !providers.translator.plainText);
|
|
553
|
+
// A plain-text translator drops line breaks: it gets one line at a time.
|
|
554
|
+
const sender = isLlm ? translator : (0, line_breaks_1.translateLineByLine)(translator);
|
|
552
555
|
// Scripts FLORES-200 lacks (sr-Latn, uz-Cyrl) only work with an LLM.
|
|
553
556
|
const nllbTarget = !targetLocale || !(0, language_codes_1.isSupported)(targetLocale) || (0, language_codes_1.isNllbSupported)(targetLocale);
|
|
554
557
|
if (uncachedEntries.length > 0) {
|
|
@@ -679,7 +682,7 @@ async function translateEntries(entries, sourceLang, targetLang, projectId, onPr
|
|
|
679
682
|
.filter((t) => batch.some((b) => b.icu && (0, glossary_1.sourceHasTerm)(b.value, t)))
|
|
680
683
|
.map((t) => ({ term: t, translation: t })),
|
|
681
684
|
];
|
|
682
|
-
const keyResults = await
|
|
685
|
+
const keyResults = await sender.translateBatch({
|
|
683
686
|
entries: batch.map((b) => ({
|
|
684
687
|
key: b.key,
|
|
685
688
|
value: b.sendText,
|
|
@@ -800,7 +803,7 @@ async function translateEntries(entries, sourceLang, targetLang, projectId, onPr
|
|
|
800
803
|
await runBatches(chunk(prepared.filter((b) => !viaBranches.has(b.key)), BATCH_SIZE), isLlm ? firstPassHint : undefined);
|
|
801
804
|
if (viaBranches.size > 0) {
|
|
802
805
|
const branchEntries = prepared.filter((b) => viaBranches.has(b.key));
|
|
803
|
-
for (const [key, o] of await translateByBranches(branchEntries,
|
|
806
|
+
for (const [key, o] of await translateByBranches(branchEntries, sender))
|
|
804
807
|
outcomes.set(key, o);
|
|
805
808
|
// A count whose plural forms couldn't be built is translated as the
|
|
806
809
|
// plain message it is (as before target-side plurals), and says so.
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import type { EngineConfigV2, Phase0Result } from '../../runtime/types';
|
|
1
|
+
import type { EngineConfigV2, Phase0Result, WorkspaceInfo } from '../../runtime/types';
|
|
2
2
|
interface PackageJson {
|
|
3
3
|
dependencies?: Record<string, string>;
|
|
4
4
|
devDependencies?: Record<string, string>;
|
|
5
5
|
[key: string]: unknown;
|
|
6
6
|
}
|
|
7
7
|
export declare function validateWorkspace(config: EngineConfigV2): Promise<Phase0Result>;
|
|
8
|
+
/** Expo (Router or classic) or bare React Native. */
|
|
9
|
+
export declare function isReactNativePackage(packageJson: PackageJson | null | undefined): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The app inside a monorepo: the first workspace package (package.json
|
|
12
|
+
* `workspaces`), or else the first top-level folder, whose package.json is
|
|
13
|
+
* a framework Babelize supports. `dir` is relative to the repo root.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findWorkspaceApp(workspacePath: string, rootPackageJson: PackageJson | null | undefined): Promise<{
|
|
16
|
+
dir: string;
|
|
17
|
+
packageJson: PackageJson;
|
|
18
|
+
framework: WorkspaceInfo['framework'];
|
|
19
|
+
} | null>;
|
|
8
20
|
export declare function checkI18nDependencies(packageJson: PackageJson, framework?: string): Promise<{
|
|
9
21
|
hasReactI18next: boolean;
|
|
10
22
|
hasI18next: boolean;
|
|
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.validateWorkspace = validateWorkspace;
|
|
40
|
+
exports.isReactNativePackage = isReactNativePackage;
|
|
41
|
+
exports.findWorkspaceApp = findWorkspaceApp;
|
|
40
42
|
exports.checkI18nDependencies = checkI18nDependencies;
|
|
41
43
|
const path = __importStar(require("path"));
|
|
42
44
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
@@ -70,7 +72,7 @@ async function detectDjangoRoot(workspacePath, rootJsFramework) {
|
|
|
70
72
|
}
|
|
71
73
|
return null;
|
|
72
74
|
}
|
|
73
|
-
const SUPPORTED_LIST =
|
|
75
|
+
const SUPPORTED_LIST = stack_1.SUPPORTED_SUMMARY;
|
|
74
76
|
/** The clearest "not supported" message for a workspace we couldn't classify. */
|
|
75
77
|
async function unsupportedMessage(workspacePath, fallback) {
|
|
76
78
|
try {
|
|
@@ -137,44 +139,21 @@ async function validateWorkspace(config) {
|
|
|
137
139
|
else if (framework === 'unknown' && packageJson) {
|
|
138
140
|
framework = detectFramework(packageJson);
|
|
139
141
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (framework === 'unknown' && !isStatic) {
|
|
159
|
-
const entries = await (0, file_utils_1.readDir)(workspacePath);
|
|
160
|
-
for (const entry of entries) {
|
|
161
|
-
if (entry.startsWith('.'))
|
|
162
|
-
continue;
|
|
163
|
-
const subDirPath = path.join(workspacePath, entry);
|
|
164
|
-
if (await (0, file_utils_1.isDirectory)(subDirPath)) {
|
|
165
|
-
const subPkgPath = path.join(subDirPath, 'package.json');
|
|
166
|
-
const subPkg = await (0, file_utils_1.readJsonFile)(subPkgPath);
|
|
167
|
-
if (subPkg) {
|
|
168
|
-
const subFramework = detectFramework(subPkg);
|
|
169
|
-
if (subFramework !== 'unknown') {
|
|
170
|
-
framework = subFramework;
|
|
171
|
-
packageJson = subPkg;
|
|
172
|
-
logger_1.engineLogger.info('Framework detected in subdirectory', { subDir: entry, framework });
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
142
|
+
// A monorepo: the app is a workspace package (apps/mobile) or a top-level
|
|
143
|
+
// folder. Everything app-specific — sources, locales, the i18n config, the
|
|
144
|
+
// package.json that gets the dependencies — lives in that folder.
|
|
145
|
+
// Also with an explicit framework (the CLI and the dashboard pass the one
|
|
146
|
+
// they detected): the root package.json isn't the app, so find the package
|
|
147
|
+
// that is, or every path below would point at the repo root.
|
|
148
|
+
let appDir = '.';
|
|
149
|
+
const rootIsApp = !!packageJson && detectFramework(packageJson) !== 'unknown';
|
|
150
|
+
if (!isStatic && framework !== 'django' && !rootIsApp) {
|
|
151
|
+
const app = await findWorkspaceApp(workspacePath, packageJson);
|
|
152
|
+
if (app && (framework === 'unknown' || app.framework === framework)) {
|
|
153
|
+
framework = app.framework;
|
|
154
|
+
packageJson = app.packageJson;
|
|
155
|
+
appDir = app.dir;
|
|
156
|
+
logger_1.engineLogger.info('Framework detected in workspace', { workspaceDir: app.dir, framework });
|
|
178
157
|
}
|
|
179
158
|
}
|
|
180
159
|
if (!types_1.SUPPORTED_FRAMEWORKS.includes(framework)) {
|
|
@@ -186,14 +165,38 @@ async function validateWorkspace(config) {
|
|
|
186
165
|
if (stack?.unsupported)
|
|
187
166
|
throw new Error(stack.message);
|
|
188
167
|
}
|
|
189
|
-
// Django
|
|
190
|
-
|
|
168
|
+
// Django and React Native scan the whole project (templates/ and apps live
|
|
169
|
+
// anywhere; RN keeps App.tsx at the root next to screens/, navigation/,
|
|
170
|
+
// hooks/); their excludedPatterns skip virtualenvs and native projects.
|
|
171
|
+
const sourceDirs = types_1.FRAMEWORK_CONFIGS[framework]?.scanRoot
|
|
172
|
+
? [appDir]
|
|
173
|
+
: (await findSourceDirectories(path.join(workspacePath, appDir))).map((d) => path.join(appDir, d));
|
|
191
174
|
if (sourceDirs.length === 0) {
|
|
192
|
-
logger_1.engineLogger.info('No standard source directories found, defaulting to root');
|
|
193
|
-
sourceDirs.push(
|
|
175
|
+
logger_1.engineLogger.info('No standard source directories found, defaulting to the app root', { appDir });
|
|
176
|
+
sourceDirs.push(appDir);
|
|
194
177
|
}
|
|
195
178
|
const frameworkConfig = types_1.FRAMEWORK_CONFIGS[framework] || types_1.FRAMEWORK_CONFIGS['unknown'];
|
|
196
179
|
const localeFormat = frameworkConfig.localeFormat;
|
|
180
|
+
let publicDir = appDir === '.' ? 'public' : path.join(appDir, 'public');
|
|
181
|
+
if (appDir === '.' && sourceDirs.length > 0 && sourceDirs[0] !== '.') {
|
|
182
|
+
const firstSourceDir = sourceDirs[0];
|
|
183
|
+
const parts = firstSourceDir.split(/[\\/]/);
|
|
184
|
+
if (parts.length > 1) {
|
|
185
|
+
parts.pop();
|
|
186
|
+
publicDir = path.join(...parts, 'public');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (framework === 'static') {
|
|
190
|
+
sourceDirs.length = 0;
|
|
191
|
+
sourceDirs.push('.');
|
|
192
|
+
publicDir = '.';
|
|
193
|
+
}
|
|
194
|
+
if (framework === 'react-native') {
|
|
195
|
+
// Translations are bundled with the app (imported by i18n.ts), not served:
|
|
196
|
+
// src/locales when the app has src/, else locales/ — never app/, which
|
|
197
|
+
// Expo Router treats as routes.
|
|
198
|
+
publicDir = (await (0, file_utils_1.isDirectory)(path.join(workspacePath, appDir, 'src'))) ? path.join(appDir, 'src') : appDir;
|
|
199
|
+
}
|
|
197
200
|
const existingLocales = [];
|
|
198
201
|
let hasLocalesDir = false;
|
|
199
202
|
let localeDirRel = '';
|
|
@@ -213,7 +216,8 @@ async function validateWorkspace(config) {
|
|
|
213
216
|
}
|
|
214
217
|
}
|
|
215
218
|
else {
|
|
216
|
-
|
|
219
|
+
// Where localeFilePath() writes them: <publicDir>/locales/<locale>.json.
|
|
220
|
+
const localesDir = path.join(workspacePath, publicDir, 'locales');
|
|
217
221
|
hasLocalesDir = await (0, file_utils_1.isDirectory)(localesDir);
|
|
218
222
|
const localeExt = (0, file_utils_1.localeExtension)(localeFormat);
|
|
219
223
|
if (hasLocalesDir) {
|
|
@@ -224,20 +228,6 @@ async function validateWorkspace(config) {
|
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
}
|
|
227
|
-
let publicDir = 'public';
|
|
228
|
-
if (sourceDirs.length > 0 && sourceDirs[0] !== '.') {
|
|
229
|
-
const firstSourceDir = sourceDirs[0];
|
|
230
|
-
const parts = firstSourceDir.split(/[\\/]/);
|
|
231
|
-
if (parts.length > 1) {
|
|
232
|
-
parts.pop();
|
|
233
|
-
publicDir = path.join(...parts, 'public');
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (framework === 'static') {
|
|
237
|
-
sourceDirs.length = 0;
|
|
238
|
-
sourceDirs.push('.');
|
|
239
|
-
publicDir = '.';
|
|
240
|
-
}
|
|
241
231
|
if (localeFormat === 'po') {
|
|
242
232
|
// For gettext projects publicDir is the locale directory.
|
|
243
233
|
publicDir = localeDirRel;
|
|
@@ -252,6 +242,8 @@ async function validateWorkspace(config) {
|
|
|
252
242
|
hasLocalesDir,
|
|
253
243
|
existingLocales,
|
|
254
244
|
publicDir,
|
|
245
|
+
...(appDir !== '.' ? { appDir } : {}),
|
|
246
|
+
...(frameworkConfig.excludedPatterns ? { excludedPatterns: frameworkConfig.excludedPatterns } : {}),
|
|
255
247
|
packageJson: (packageJson ?? {}),
|
|
256
248
|
formatConfig: config.formatConfig,
|
|
257
249
|
...(framework === 'django' ? { djangoRoot: djangoRoot ?? '.' } : {}),
|
|
@@ -270,6 +262,10 @@ function detectFramework(packageJson) {
|
|
|
270
262
|
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
271
263
|
if (deps['next'])
|
|
272
264
|
return 'nextjs';
|
|
265
|
+
// Before react: every Expo / React Native app also depends on react (and
|
|
266
|
+
// Expo Router apps on react-dom for the web target).
|
|
267
|
+
if (isReactNativePackage(packageJson))
|
|
268
|
+
return 'react-native';
|
|
273
269
|
if (deps['nuxt'])
|
|
274
270
|
return 'vue';
|
|
275
271
|
if (deps['vue'])
|
|
@@ -296,6 +292,40 @@ function detectFramework(packageJson) {
|
|
|
296
292
|
return 'react';
|
|
297
293
|
return 'unknown';
|
|
298
294
|
}
|
|
295
|
+
/** Expo (Router or classic) or bare React Native. */
|
|
296
|
+
function isReactNativePackage(packageJson) {
|
|
297
|
+
const deps = { ...packageJson?.dependencies, ...packageJson?.devDependencies };
|
|
298
|
+
return Boolean(deps['expo'] || deps['react-native']);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* The app inside a monorepo: the first workspace package (package.json
|
|
302
|
+
* `workspaces`), or else the first top-level folder, whose package.json is
|
|
303
|
+
* a framework Babelize supports. `dir` is relative to the repo root.
|
|
304
|
+
*/
|
|
305
|
+
async function findWorkspaceApp(workspacePath, rootPackageJson) {
|
|
306
|
+
const candidates = [];
|
|
307
|
+
const workspacePatterns = getWorkspacePatterns(rootPackageJson);
|
|
308
|
+
if (workspacePatterns.length > 0) {
|
|
309
|
+
candidates.push(...(await (0, fast_glob_1.default)(workspacePatterns, { cwd: workspacePath, onlyDirectories: true, deep: 3, absolute: true })).sort());
|
|
310
|
+
}
|
|
311
|
+
for (const entry of await (0, file_utils_1.readDir)(workspacePath)) {
|
|
312
|
+
if (entry.startsWith('.') || entry === 'node_modules')
|
|
313
|
+
continue;
|
|
314
|
+
const dir = path.join(workspacePath, entry);
|
|
315
|
+
if (await (0, file_utils_1.isDirectory)(dir))
|
|
316
|
+
candidates.push(dir);
|
|
317
|
+
}
|
|
318
|
+
for (const dir of candidates) {
|
|
319
|
+
const subPkg = await (0, file_utils_1.readJsonFile)(path.join(dir, 'package.json'));
|
|
320
|
+
if (!subPkg)
|
|
321
|
+
continue;
|
|
322
|
+
const framework = detectFramework(subPkg);
|
|
323
|
+
if (framework !== 'unknown') {
|
|
324
|
+
return { dir: path.relative(workspacePath, dir).split(path.sep).join('/'), packageJson: subPkg, framework };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
299
329
|
async function findSourceDirectories(workspacePath) {
|
|
300
330
|
const possibleDirs = ['src', 'app', 'pages', 'components', 'features', 'modules', 'lib'];
|
|
301
331
|
const foundDirs = [];
|
|
@@ -322,6 +352,13 @@ async function checkI18nDependencies(packageJson, framework) {
|
|
|
322
352
|
if (!deps['vue-i18n'])
|
|
323
353
|
recommendation = 'Run: bun install vue-i18n';
|
|
324
354
|
}
|
|
355
|
+
else if (framework === 'react-native') {
|
|
356
|
+
if (!hasI18next || !hasReactI18next) {
|
|
357
|
+
recommendation = deps['expo']
|
|
358
|
+
? 'Run: npx expo install i18next react-i18next expo-localization'
|
|
359
|
+
: 'Run: npm install i18next react-i18next react-native-localize';
|
|
360
|
+
}
|
|
361
|
+
}
|
|
325
362
|
else {
|
|
326
363
|
// React / vanilla / unknown
|
|
327
364
|
if (!hasI18next)
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -87,6 +87,14 @@ export interface WorkspaceInfo {
|
|
|
87
87
|
hasLocalesDir: boolean;
|
|
88
88
|
existingLocales: string[];
|
|
89
89
|
publicDir: string;
|
|
90
|
+
/**
|
|
91
|
+
* The app's folder in a monorepo (`apps/mobile`), relative to the
|
|
92
|
+
* workspace; unset when the app is the repository root. Its package.json,
|
|
93
|
+
* tsconfig and entry points are the app's; installs still run at the root.
|
|
94
|
+
*/
|
|
95
|
+
appDir?: string;
|
|
96
|
+
/** Framework-specific scan exclusions (FrameworkConfig.excludedPatterns); see workspaceExcludes(). */
|
|
97
|
+
excludedPatterns?: readonly string[];
|
|
90
98
|
packageJson?: Record<string, any>;
|
|
91
99
|
/** Output locale file format (e.g., 'json' for React, 'po' for Django) */
|
|
92
100
|
localeFormat: 'json' | 'po' | 'arb';
|
|
@@ -424,4 +432,18 @@ export interface Phase7Result {
|
|
|
424
432
|
}
|
|
425
433
|
export declare const SUPPORTED_EXTENSIONS: string[];
|
|
426
434
|
export declare const EXCLUDED_PATTERNS: string[];
|
|
435
|
+
/**
|
|
436
|
+
* Exclusions for scanning a workspace's sources: the engine defaults plus the
|
|
437
|
+
* framework's own (virtualenvs for Django, the native ios/android projects for
|
|
438
|
+
* React Native). Every source scan goes through this so they can't disagree.
|
|
439
|
+
*/
|
|
440
|
+
export declare function workspaceExcludes(workspace: {
|
|
441
|
+
excludedPatterns?: readonly string[];
|
|
442
|
+
}): string[];
|
|
443
|
+
/**
|
|
444
|
+
* Whether a workspace-relative path falls under one of `patterns`, read the
|
|
445
|
+
* way findFiles reads them: a leading `/` anchors to the root (`/ios`), other
|
|
446
|
+
* names match any path segment (`Pods`), `*` globs match file names.
|
|
447
|
+
*/
|
|
448
|
+
export declare function isExcludedPath(relPath: string, patterns: readonly string[]): boolean;
|
|
427
449
|
export declare const JSX_TRANSLATABLE_ATTRS: string[];
|
package/dist/runtime/types.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.JSX_TRANSLATABLE_ATTRS = exports.EXCLUDED_PATTERNS = exports.SUPPORTED_EXTENSIONS = void 0;
|
|
11
|
+
exports.workspaceExcludes = workspaceExcludes;
|
|
12
|
+
exports.isExcludedPath = isExcludedPath;
|
|
11
13
|
// ============ Constants ============
|
|
12
14
|
exports.SUPPORTED_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.html'];
|
|
13
15
|
exports.EXCLUDED_PATTERNS = [
|
|
@@ -27,12 +29,43 @@ exports.EXCLUDED_PATTERNS = [
|
|
|
27
29
|
'*.min.js',
|
|
28
30
|
'*.min.css',
|
|
29
31
|
];
|
|
32
|
+
/**
|
|
33
|
+
* Exclusions for scanning a workspace's sources: the engine defaults plus the
|
|
34
|
+
* framework's own (virtualenvs for Django, the native ios/android projects for
|
|
35
|
+
* React Native). Every source scan goes through this so they can't disagree.
|
|
36
|
+
*/
|
|
37
|
+
function workspaceExcludes(workspace) {
|
|
38
|
+
return [...exports.EXCLUDED_PATTERNS, ...(workspace.excludedPatterns ?? [])];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether a workspace-relative path falls under one of `patterns`, read the
|
|
42
|
+
* way findFiles reads them: a leading `/` anchors to the root (`/ios`), other
|
|
43
|
+
* names match any path segment (`Pods`), `*` globs match file names.
|
|
44
|
+
*/
|
|
45
|
+
function isExcludedPath(relPath, patterns) {
|
|
46
|
+
const segments = relPath.split(/[\\/]/).filter(Boolean);
|
|
47
|
+
const rel = segments.join('/');
|
|
48
|
+
return patterns.some((pattern) => {
|
|
49
|
+
if (pattern.includes('*')) {
|
|
50
|
+
const re = new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`);
|
|
51
|
+
return segments.some((segment) => re.test(segment));
|
|
52
|
+
}
|
|
53
|
+
if (pattern.includes('/')) {
|
|
54
|
+
const anchored = pattern.startsWith('/') ? pattern.slice(1) : pattern;
|
|
55
|
+
return rel === anchored || rel.startsWith(`${anchored}/`);
|
|
56
|
+
}
|
|
57
|
+
return segments.includes(pattern);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
30
60
|
exports.JSX_TRANSLATABLE_ATTRS = [
|
|
31
61
|
'title',
|
|
32
62
|
'placeholder',
|
|
33
63
|
'alt',
|
|
34
64
|
'aria-label',
|
|
35
65
|
'aria-description',
|
|
66
|
+
// React Native's screen-reader props (the web uses aria-*).
|
|
67
|
+
'accessibilityLabel',
|
|
68
|
+
'accessibilityHint',
|
|
36
69
|
'label',
|
|
37
70
|
'description',
|
|
38
71
|
];
|
|
@@ -13,7 +13,18 @@ export interface ParseOptions {
|
|
|
13
13
|
isTypeScript?: boolean;
|
|
14
14
|
isJSX?: boolean;
|
|
15
15
|
filename?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Parse for rewriting: `generateCode` then reprints only the nodes that
|
|
18
|
+
* changed and keeps every other line of the file exactly as written.
|
|
19
|
+
*/
|
|
20
|
+
preserveFormat?: boolean;
|
|
16
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Where a node starts in the file as written. Recast hands the parser a
|
|
24
|
+
* normalized copy (`\n` line endings, leading tabs expanded), so the node's
|
|
25
|
+
* own offset can differ from the source offsets extraction recorded.
|
|
26
|
+
*/
|
|
27
|
+
export declare function sourceOffset(ast: t.File, node: t.Node): number | undefined;
|
|
17
28
|
export declare function parseCode(code: string, options?: ParseOptions): t.File;
|
|
18
29
|
export declare function generateCode(ast: t.File, originalCode?: string): string;
|
|
19
30
|
/**
|
|
@@ -26,6 +37,11 @@ export declare function placeStatementsOnOwnLines(code: string, statements: stri
|
|
|
26
37
|
export declare function isJSXText(node: t.Node): node is t.JSXText;
|
|
27
38
|
export declare function isAlreadyTranslated(node: t.Node, path: NodePath): boolean;
|
|
28
39
|
export declare function isNonTranslatableContext(path: NodePath): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Expressions a value passes through unchanged on its way to its slot:
|
|
42
|
+
* `a ? 'x' : b`, `k ?? 'x'`, `('x' as K)`, `x!`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isValuePassthrough(node: t.Node, child: t.Node): boolean;
|
|
29
45
|
export declare function getComponentName(path: NodePath): string | null;
|
|
30
46
|
export declare function isInsideComponent(path: NodePath): boolean;
|
|
31
47
|
/**
|
|
@@ -33,7 +49,11 @@ export declare function isInsideComponent(path: NodePath): boolean;
|
|
|
33
49
|
* goes. Skips callbacks (a hook inside `.map(…)` breaks the rules of hooks);
|
|
34
50
|
* null when there's no component with a block body.
|
|
35
51
|
*/
|
|
36
|
-
export declare function findComponentBody(path: NodePath
|
|
52
|
+
export declare function findComponentBody(path: NodePath,
|
|
53
|
+
/** Also lowercase functions returning JSX (render helpers), for when there is no module-level t. */
|
|
54
|
+
options?: {
|
|
55
|
+
renderHelpers?: boolean;
|
|
56
|
+
}): NodePath | null;
|
|
37
57
|
export declare function hasTranslationImport(ast: t.File): boolean;
|
|
38
58
|
export declare function hasTranslationHook(functionPath: NodePath): boolean;
|
|
39
59
|
export declare function createTranslationImport(): t.ImportDeclaration;
|
|
@@ -45,7 +65,13 @@ export declare function getParentContext(path: NodePath): {
|
|
|
45
65
|
parentContext: string;
|
|
46
66
|
siblingGroupId: string;
|
|
47
67
|
} | null;
|
|
48
|
-
|
|
68
|
+
/**
|
|
69
|
+
* A string-building expression as one message: `\`Hi ${user.name}\`` and
|
|
70
|
+
* `'Hi ' + name` → "Hi {name}". `variables` receives the placeholder names
|
|
71
|
+
* and, when given, `expressions` the expression behind each one (same order),
|
|
72
|
+
* so injection can rebuild the message as `t(key, { name: user.name })`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function extractExpressionContent(node: t.Node, variables?: string[], expressions?: t.Expression[]): string | null;
|
|
49
75
|
export declare function extractConditionalContent(node: t.ConditionalExpression): {
|
|
50
76
|
consequent: string | null;
|
|
51
77
|
alternate: string | null;
|
|
@@ -55,3 +81,5 @@ export declare function extractConditionalContent(node: t.ConditionalExpression)
|
|
|
55
81
|
*/
|
|
56
82
|
export declare function hasUseClientDirective(ast: t.File): boolean;
|
|
57
83
|
export { generate, t, traverse };
|
|
84
|
+
/** A top-level import or declaration already binds `name` (e.g. `t` from an earlier run's `@/i18n` import). */
|
|
85
|
+
export declare function bindsTopLevel(ast: t.File, name: string): boolean;
|