@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
|
@@ -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;
|
|
@@ -344,10 +334,12 @@ async function finalizeProject(workspace, dryRun, options = {}) {
|
|
|
344
334
|
}
|
|
345
335
|
else {
|
|
346
336
|
// 'missing' or 'ours' — write/overwrite
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
337
|
+
// sourceDirs[0] is the scan root ("."), not a locale. Targets: this run's plus any
|
|
338
|
+
// locale files already on disk, so the switcher lists every language.
|
|
339
|
+
const sourceLocale = options.sourceLocale || 'en';
|
|
340
|
+
const targetLocales = [...new Set([...(options.targetLocales ?? []), ...(workspace.existingLocales ?? [])])]
|
|
341
|
+
.filter((l) => l && l !== sourceLocale && /^[A-Za-z]{2,3}([-_][A-Za-z0-9]+)*$/.test(l));
|
|
342
|
+
const runtimeContent = (0, static_runtime_1.buildStaticRuntime)({ sourceLocale, targetLocales });
|
|
351
343
|
await writer.write(i18nJsPath, runtimeContent);
|
|
352
344
|
configFileCreated = true;
|
|
353
345
|
if (regenerationStatus === 'ours') {
|
|
@@ -379,6 +371,16 @@ async function finalizeProject(workspace, dryRun, options = {}) {
|
|
|
379
371
|
}
|
|
380
372
|
}
|
|
381
373
|
}
|
|
374
|
+
else if (framework === 'react-native') {
|
|
375
|
+
const rn = await (0, react_native_finalize_1.finalizeReactNative)(workspace, {
|
|
376
|
+
sourceLocale: options.sourceLocale,
|
|
377
|
+
targetLocales: options.targetLocales,
|
|
378
|
+
}, writer);
|
|
379
|
+
warnings.push(...rn.warnings);
|
|
380
|
+
userOwnsInit = rn.userOwnsInit;
|
|
381
|
+
configFileCreated = configFileCreated || rn.configFileCreated;
|
|
382
|
+
entryPointModified = entryPointModified || rn.entryPointModified;
|
|
383
|
+
}
|
|
382
384
|
else if (useNextIntl) {
|
|
383
385
|
const nextIntl = await (0, nextjs_finalize_1.finalizeNextIntl)(workspace, dryRun, {
|
|
384
386
|
sourceLocale: options.sourceLocale,
|
|
@@ -412,7 +414,7 @@ async function finalizeProject(workspace, dryRun, options = {}) {
|
|
|
412
414
|
userOwnsInit = icu.userOwnsInit;
|
|
413
415
|
if (!userOwnsInit && !(await (0, file_utils_1.pathExists)(i18nPath)) && !(await (0, file_utils_1.pathExists)(i18nJsPath))) {
|
|
414
416
|
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')));
|
|
417
|
+
(await (0, file_utils_1.pathExists)(path.join(workspace.path, workspace.appDir ?? '.', 'tsconfig.json')));
|
|
416
418
|
const targetPath = isTS ? i18nPath : i18nJsPath;
|
|
417
419
|
const template = I18N_CONFIG_TEMPLATES[framework] || I18N_CONFIG_TEMPLATES['unknown'];
|
|
418
420
|
await writer.write(targetPath, template);
|
|
@@ -450,7 +452,7 @@ async function finalizeProject(workspace, dryRun, options = {}) {
|
|
|
450
452
|
];
|
|
451
453
|
if (framework === 'vue') {
|
|
452
454
|
for (const entry of entryPoints) {
|
|
453
|
-
const entryPath = path.join(workspace.path, entry);
|
|
455
|
+
const entryPath = path.join(workspace.path, workspace.appDir ?? '.', entry);
|
|
454
456
|
if (await (0, file_utils_1.pathExists)(entryPath)) {
|
|
455
457
|
const content = await (0, file_utils_1.readFile)(entryPath);
|
|
456
458
|
const entryDir = path.dirname(entryPath);
|
|
@@ -468,9 +470,9 @@ async function finalizeProject(workspace, dryRun, options = {}) {
|
|
|
468
470
|
}
|
|
469
471
|
}
|
|
470
472
|
}
|
|
471
|
-
else if (!['static', 'django'].includes(framework) && !useNextIntl && !userOwnsInit) {
|
|
473
|
+
else if (!['static', 'django', 'react-native'].includes(framework) && !useNextIntl && !userOwnsInit) {
|
|
472
474
|
for (const entry of entryPoints) {
|
|
473
|
-
const entryPath = path.join(workspace.path, entry);
|
|
475
|
+
const entryPath = path.join(workspace.path, workspace.appDir ?? '.', entry);
|
|
474
476
|
if (await (0, file_utils_1.pathExists)(entryPath)) {
|
|
475
477
|
const content = await (0, file_utils_1.readFile)(entryPath);
|
|
476
478
|
const entryDir = path.dirname(entryPath);
|
|
@@ -515,7 +517,7 @@ function modifyNextjsLayout(content, relativeI18nPath) {
|
|
|
515
517
|
const providerImportPath = relativeI18nPath.replace(/i18n(\.(ts|js))?$/, 'I18nProvider');
|
|
516
518
|
const isTS = content.includes(': Readonly') || content.includes(': React.ReactNode');
|
|
517
519
|
try {
|
|
518
|
-
const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript: isTS });
|
|
520
|
+
const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript: isTS, preserveFormat: true });
|
|
519
521
|
let modified = false;
|
|
520
522
|
let wrapped = false;
|
|
521
523
|
const sideEffectImports = [
|
|
@@ -596,7 +598,7 @@ function modifyNextjsLayoutFallback(content, providerImportPath) {
|
|
|
596
598
|
function modifyVueEntryPoint(content, relativeI18nPath) {
|
|
597
599
|
const isTS = /\.tsx?$/.test(relativeI18nPath) || /\bfrom\s+['"]vue['"]/.test(content) || /:\s*(?:string|number|boolean|Ref|ComputedRef)/.test(content);
|
|
598
600
|
try {
|
|
599
|
-
const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript: isTS });
|
|
601
|
+
const ast = (0, ast_helpers_1.parseCode)(content, { isTypeScript: isTS, preserveFormat: true });
|
|
600
602
|
let hasExistingImport = false;
|
|
601
603
|
let modified = false;
|
|
602
604
|
for (const node of ast.program.body) {
|
|
@@ -739,7 +741,9 @@ function i18nRuntimeFor(framework, pkg) {
|
|
|
739
741
|
return { library: 'Django gettext', required: [], missing: [] };
|
|
740
742
|
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
741
743
|
const nextIntl = fw === 'nextjs' && !deps['react-i18next'];
|
|
742
|
-
const required = Object.keys(nextIntl ? nextjs_finalize_1.NEXT_INTL_DEPENDENCIES
|
|
744
|
+
const required = Object.keys(nextIntl ? nextjs_finalize_1.NEXT_INTL_DEPENDENCIES
|
|
745
|
+
: fw === 'react-native' ? (0, react_native_finalize_1.reactNativeDependencies)(pkg ?? undefined).deps
|
|
746
|
+
: DEPENDENCIES_BY_FRAMEWORK[fw] ?? {});
|
|
743
747
|
const library = nextIntl ? 'next-intl' : fw === 'vue' ? 'vue-i18n' : 'react-i18next';
|
|
744
748
|
return { library, required, missing: required.filter((d) => !(pkg?.dependencies ?? {})[d]) };
|
|
745
749
|
}
|
|
@@ -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')
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { WorkspaceInfo } from '../../runtime/types';
|
|
2
|
+
import { FinalizationWriter } from './writes';
|
|
3
|
+
interface PackageJson {
|
|
4
|
+
main?: string;
|
|
5
|
+
dependencies?: Record<string, string>;
|
|
6
|
+
devDependencies?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export declare function isExpoProject(pkg: PackageJson | undefined): boolean;
|
|
9
|
+
/** The Expo SDK major from the `expo` dependency range (`~57.0.30` → 57). */
|
|
10
|
+
export declare function expoSdkMajor(range: string | undefined): number | null;
|
|
11
|
+
/** expo-localization range for an Expo SDK; null when the SDK can't be read. */
|
|
12
|
+
export declare function expoLocalizationFor(expoRange: string | undefined): string | null;
|
|
13
|
+
/** Dependencies a React Native app needs, and a warning when a version is a guess. */
|
|
14
|
+
export declare function reactNativeDependencies(pkg: PackageJson | undefined): {
|
|
15
|
+
deps: Record<string, string>;
|
|
16
|
+
warning?: string;
|
|
17
|
+
};
|
|
18
|
+
/** `<locales>/index.(ts|js)`: every bundled locale, source first. */
|
|
19
|
+
export declare function buildLocalesIndex(locales: string[], sourceLocale: string): string;
|
|
20
|
+
/** `i18n.(ts|js)`: i18next with the bundled resources and the device's language. */
|
|
21
|
+
export declare function buildReactNativeConfig(options: {
|
|
22
|
+
sourceLocale: string;
|
|
23
|
+
localeModule: string;
|
|
24
|
+
localesImport: string;
|
|
25
|
+
typescript: boolean;
|
|
26
|
+
}): string;
|
|
27
|
+
/**
|
|
28
|
+
* The module that runs first:
|
|
29
|
+
* - Expo Router (`main: "expo-router/entry"`): the root layout, `app/_layout`;
|
|
30
|
+
* - `main` pointing at a file (classic Expo `index.ts`, custom entries): that file;
|
|
31
|
+
* - bare React Native (Metro defaults to `index.js`): `index.*`, then `App.*`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function entryPointCandidates(pkg: PackageJson | undefined): string[];
|
|
34
|
+
export interface ReactNativeFinalizeResult {
|
|
35
|
+
configFileCreated: boolean;
|
|
36
|
+
entryPointModified: boolean;
|
|
37
|
+
/** The project initializes i18next itself: Babelize wrote no config. */
|
|
38
|
+
userOwnsInit: boolean;
|
|
39
|
+
warnings: string[];
|
|
40
|
+
}
|
|
41
|
+
export declare function finalizeReactNative(workspace: WorkspaceInfo, options: {
|
|
42
|
+
sourceLocale?: string;
|
|
43
|
+
targetLocales?: string[];
|
|
44
|
+
}, writer: FinalizationWriter): Promise<ReactNativeFinalizeResult>;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isExpoProject = isExpoProject;
|
|
37
|
+
exports.expoSdkMajor = expoSdkMajor;
|
|
38
|
+
exports.expoLocalizationFor = expoLocalizationFor;
|
|
39
|
+
exports.reactNativeDependencies = reactNativeDependencies;
|
|
40
|
+
exports.buildLocalesIndex = buildLocalesIndex;
|
|
41
|
+
exports.buildReactNativeConfig = buildReactNativeConfig;
|
|
42
|
+
exports.entryPointCandidates = entryPointCandidates;
|
|
43
|
+
exports.finalizeReactNative = finalizeReactNative;
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const file_utils_1 = require("../../utils/file-utils");
|
|
46
|
+
const i18next_icu_1 = require("../../utils/i18next-icu");
|
|
47
|
+
const i18n_config_path_1 = require("../../utils/i18n-config-path");
|
|
48
|
+
const dependencies_1 = require("./dependencies");
|
|
49
|
+
/**
|
|
50
|
+
* React Native finalization (Expo Router, classic Expo, bare React Native).
|
|
51
|
+
*
|
|
52
|
+
* A native app can't fetch `/locales/<lng>.json` from a server, and has no
|
|
53
|
+
* browser language. So:
|
|
54
|
+
* - translations are bundled: `<locales>/index.(ts|js)` imports every
|
|
55
|
+
* `<locales>/<locale>.json` and is rewritten on every run (languages come
|
|
56
|
+
* and go);
|
|
57
|
+
* - `i18n.(ts|js)` initializes i18next from those resources with the
|
|
58
|
+
* device's language (expo-localization, or react-native-localize in bare
|
|
59
|
+
* React Native). It is written once; the user may edit it;
|
|
60
|
+
* - the app's entry point imports `i18n` first, so i18next is ready before
|
|
61
|
+
* any screen renders or module-level `t()` runs.
|
|
62
|
+
* Everything goes next to the locales (`src/` or the root), never in Expo
|
|
63
|
+
* Router's `app/`, where every file is a route.
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* expo-localization for each Expo SDK, from `expo/bundledNativeModules.json`
|
|
67
|
+
* (what `npx expo install` picks). From SDK 55 the module versions follow the
|
|
68
|
+
* SDK's major version.
|
|
69
|
+
*/
|
|
70
|
+
const EXPO_LOCALIZATION_BY_SDK = {
|
|
71
|
+
50: '~14.8.4',
|
|
72
|
+
51: '~15.0.3',
|
|
73
|
+
52: '~16.0.1',
|
|
74
|
+
53: '~16.1.6',
|
|
75
|
+
54: '~17.0.9',
|
|
76
|
+
};
|
|
77
|
+
const FIRST_SDK_WITH_MATCHING_MAJORS = 55;
|
|
78
|
+
const REACT_NATIVE_LOCALIZE = '^3.7.2';
|
|
79
|
+
function allDeps(pkg) {
|
|
80
|
+
return { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
81
|
+
}
|
|
82
|
+
function isExpoProject(pkg) {
|
|
83
|
+
return Boolean(allDeps(pkg)['expo']);
|
|
84
|
+
}
|
|
85
|
+
/** The Expo SDK major from the `expo` dependency range (`~57.0.30` → 57). */
|
|
86
|
+
function expoSdkMajor(range) {
|
|
87
|
+
const m = range?.match(/(\d+)\./);
|
|
88
|
+
return m ? Number(m[1]) : null;
|
|
89
|
+
}
|
|
90
|
+
/** expo-localization range for an Expo SDK; null when the SDK can't be read. */
|
|
91
|
+
function expoLocalizationFor(expoRange) {
|
|
92
|
+
const sdk = expoSdkMajor(expoRange);
|
|
93
|
+
if (sdk === null)
|
|
94
|
+
return null;
|
|
95
|
+
if (sdk >= FIRST_SDK_WITH_MATCHING_MAJORS)
|
|
96
|
+
return `~${sdk}.0.0`;
|
|
97
|
+
return EXPO_LOCALIZATION_BY_SDK[sdk] ?? null;
|
|
98
|
+
}
|
|
99
|
+
/** Dependencies a React Native app needs, and a warning when a version is a guess. */
|
|
100
|
+
function reactNativeDependencies(pkg) {
|
|
101
|
+
const deps = { ...dependencies_1.I18NEXT_CORE_DEPENDENCIES };
|
|
102
|
+
if (!isExpoProject(pkg)) {
|
|
103
|
+
deps['react-native-localize'] = REACT_NATIVE_LOCALIZE;
|
|
104
|
+
return { deps };
|
|
105
|
+
}
|
|
106
|
+
const expoRange = allDeps(pkg)['expo'];
|
|
107
|
+
const version = expoLocalizationFor(expoRange);
|
|
108
|
+
if (version) {
|
|
109
|
+
deps['expo-localization'] = version;
|
|
110
|
+
return { deps };
|
|
111
|
+
}
|
|
112
|
+
deps['expo-localization'] = '*';
|
|
113
|
+
return {
|
|
114
|
+
deps,
|
|
115
|
+
warning: `Couldn't tell the Expo SDK from "expo": "${expoRange}". Run \`npx expo install --fix\` so expo-localization matches your SDK.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Where the device language comes from. */
|
|
119
|
+
function localeModule(pkg) {
|
|
120
|
+
return isExpoProject(pkg) ? 'expo-localization' : 'react-native-localize';
|
|
121
|
+
}
|
|
122
|
+
/** A valid identifier for a locale's messages: `pt-BR` → `ptBRMessages`. */
|
|
123
|
+
function messagesIdentifier(locale) {
|
|
124
|
+
const camel = locale
|
|
125
|
+
.split(/[^A-Za-z0-9]+/)
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
128
|
+
.join('');
|
|
129
|
+
return `${/^[0-9]/.test(camel) ? `_${camel}` : camel}Messages`;
|
|
130
|
+
}
|
|
131
|
+
const LOCALES_INDEX_MARKER = '// @babelize-generated locale index';
|
|
132
|
+
/** `<locales>/index.(ts|js)`: every bundled locale, source first. */
|
|
133
|
+
function buildLocalesIndex(locales, sourceLocale) {
|
|
134
|
+
const ordered = [sourceLocale, ...locales.filter((l) => l !== sourceLocale).sort()];
|
|
135
|
+
const imports = ordered.map((l) => `import ${messagesIdentifier(l)} from './${l}.json';`);
|
|
136
|
+
const entries = ordered.map((l) => ` ${JSON.stringify(l)}: { translation: ${messagesIdentifier(l)} },`);
|
|
137
|
+
return [
|
|
138
|
+
`${LOCALES_INDEX_MARKER}: rewritten by Babelize on every run, edit the JSON files instead.`,
|
|
139
|
+
...imports,
|
|
140
|
+
'',
|
|
141
|
+
'export const resources = {',
|
|
142
|
+
...entries,
|
|
143
|
+
'};',
|
|
144
|
+
'',
|
|
145
|
+
].join('\n');
|
|
146
|
+
}
|
|
147
|
+
/** `i18n.(ts|js)`: i18next with the bundled resources and the device's language. */
|
|
148
|
+
function buildReactNativeConfig(options) {
|
|
149
|
+
const { sourceLocale, localeModule: from, localesImport, typescript } = options;
|
|
150
|
+
const ret = typescript ? ': string' : '';
|
|
151
|
+
return `${i18next_icu_1.GENERATED_I18N_CONFIG_MARKER}
|
|
152
|
+
import i18n from 'i18next';
|
|
153
|
+
import { initReactI18next } from 'react-i18next';
|
|
154
|
+
import ICU from 'i18next-icu';
|
|
155
|
+
import { getLocales } from '${from}';
|
|
156
|
+
|
|
157
|
+
import { resources } from '${localesImport}';
|
|
158
|
+
|
|
159
|
+
const SOURCE_LOCALE = ${JSON.stringify(sourceLocale)};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The device's preferred language that has translations: the full tag
|
|
163
|
+
* (zh-Hant-TW), then shorter ones (zh-Hant), then any variant of the
|
|
164
|
+
* language (zh-Hans for zh).
|
|
165
|
+
*/
|
|
166
|
+
function deviceLanguage()${ret} {
|
|
167
|
+
const available = Object.keys(resources);
|
|
168
|
+
const find = (tag${ret}) => available.find((l) => l.toLowerCase() === tag.toLowerCase());
|
|
169
|
+
for (const locale of getLocales()) {
|
|
170
|
+
const parts = locale.languageTag.split('-');
|
|
171
|
+
for (let i = parts.length; i > 0; i--) {
|
|
172
|
+
const match = find(parts.slice(0, i).join('-'));
|
|
173
|
+
if (match) return match;
|
|
174
|
+
}
|
|
175
|
+
const sameLanguage = available.find((l) => l.split('-')[0].toLowerCase() === parts[0].toLowerCase());
|
|
176
|
+
if (sameLanguage) return sameLanguage;
|
|
177
|
+
}
|
|
178
|
+
return SOURCE_LOCALE;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
i18n
|
|
182
|
+
.use(ICU)
|
|
183
|
+
.use(initReactI18next)
|
|
184
|
+
.init({
|
|
185
|
+
resources,
|
|
186
|
+
lng: deviceLanguage(),
|
|
187
|
+
fallbackLng: SOURCE_LOCALE,
|
|
188
|
+
// Translations are bundled: initialize synchronously so t() works before the first render.
|
|
189
|
+
initAsync: false,
|
|
190
|
+
interpolation: {
|
|
191
|
+
escapeValue: false,
|
|
192
|
+
},
|
|
193
|
+
react: {
|
|
194
|
+
useSuspense: false,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export const t = i18n.t.bind(i18n);
|
|
199
|
+
export default i18n;
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
const SOURCE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'];
|
|
203
|
+
function withExtensions(base) {
|
|
204
|
+
return SOURCE_EXTENSIONS.map((ext) => `${base}${ext}`);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The module that runs first:
|
|
208
|
+
* - Expo Router (`main: "expo-router/entry"`): the root layout, `app/_layout`;
|
|
209
|
+
* - `main` pointing at a file (classic Expo `index.ts`, custom entries): that file;
|
|
210
|
+
* - bare React Native (Metro defaults to `index.js`): `index.*`, then `App.*`.
|
|
211
|
+
*/
|
|
212
|
+
function entryPointCandidates(pkg) {
|
|
213
|
+
const main = pkg?.main?.replace(/^\.\//, '');
|
|
214
|
+
if (main && /^expo-router\//.test(main)) {
|
|
215
|
+
return [...withExtensions('src/app/_layout'), ...withExtensions('app/_layout')];
|
|
216
|
+
}
|
|
217
|
+
const candidates = [];
|
|
218
|
+
// A package's entry ("expo/AppEntry.js", classic Expo before SDK 49) isn't
|
|
219
|
+
// in the project (or is in node_modules): the app is App.* then.
|
|
220
|
+
if (main && !main.includes('node_modules')) {
|
|
221
|
+
candidates.push(...(path.extname(main) ? [main] : withExtensions(main)));
|
|
222
|
+
}
|
|
223
|
+
candidates.push(...withExtensions('index'), ...withExtensions('App'), ...withExtensions('src/App'));
|
|
224
|
+
return [...new Set(candidates)];
|
|
225
|
+
}
|
|
226
|
+
function importsConfig(content, specifier) {
|
|
227
|
+
const escaped = specifier.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
228
|
+
return new RegExp(`(?:import|require)\\s*\\(?\\s*['"]${escaped}(?:\\.(?:[jt]sx?))?['"]`).test(content);
|
|
229
|
+
}
|
|
230
|
+
/** Every locale with a bundled JSON file, plus this run's. */
|
|
231
|
+
async function bundledLocales(workspace, sourceLocale, targetLocales) {
|
|
232
|
+
const { dir } = (0, file_utils_1.localeFilePath)(workspace.path, workspace.publicDir, 'json', sourceLocale);
|
|
233
|
+
const onDisk = (await (0, file_utils_1.pathExists)(dir))
|
|
234
|
+
? (await (0, file_utils_1.readDir)(dir)).filter((f) => f.endsWith('.json')).map((f) => f.slice(0, -'.json'.length))
|
|
235
|
+
: [];
|
|
236
|
+
return [...new Set([sourceLocale, ...targetLocales, ...onDisk])];
|
|
237
|
+
}
|
|
238
|
+
async function finalizeReactNative(workspace, options, writer) {
|
|
239
|
+
const warnings = [];
|
|
240
|
+
let configFileCreated = false;
|
|
241
|
+
let entryPointModified = false;
|
|
242
|
+
const pkg = workspace.packageJson;
|
|
243
|
+
const sourceLocale = options.sourceLocale || 'en';
|
|
244
|
+
// The app's folder: the repo root, or apps/mobile in a monorepo.
|
|
245
|
+
const appDir = (workspace.appDir ?? '.').split(path.sep).join('/');
|
|
246
|
+
const typescript = await (0, file_utils_1.pathExists)(path.join(workspace.path, appDir, 'tsconfig.json'));
|
|
247
|
+
const ext = typescript ? '.ts' : '.js';
|
|
248
|
+
const configDir = (0, i18n_config_path_1.i18nConfigDir)(workspace);
|
|
249
|
+
const { dir: localesDir } = (0, file_utils_1.localeFilePath)(workspace.path, workspace.publicDir, 'json', sourceLocale);
|
|
250
|
+
const localesRel = path.relative(workspace.path, localesDir).split(path.sep).join('/');
|
|
251
|
+
// The bundled resources: regenerated every run so added/removed languages are wired.
|
|
252
|
+
const locales = await bundledLocales(workspace, sourceLocale, options.targetLocales ?? []);
|
|
253
|
+
const indexPath = path.join(localesDir, `index${ext}`);
|
|
254
|
+
const existingIndex = await Promise.all(SOURCE_EXTENSIONS.map(async (e) => {
|
|
255
|
+
const p = path.join(localesDir, `index${e}`);
|
|
256
|
+
return (await (0, file_utils_1.pathExists)(p)) ? { path: p, content: await (0, file_utils_1.readFile)(p) } : null;
|
|
257
|
+
}));
|
|
258
|
+
const userIndex = existingIndex.find((f) => f && !f.content.startsWith(LOCALES_INDEX_MARKER));
|
|
259
|
+
if (userIndex) {
|
|
260
|
+
// Babelize's i18n config imports `resources` from this index: with the
|
|
261
|
+
// user's own index it would get undefined and crash at startup. Leave the
|
|
262
|
+
// whole setup to the user.
|
|
263
|
+
warnings.push(`${path.relative(workspace.path, userIndex.path)} isn't Babelize's, so Babelize didn't set up i18next. ` +
|
|
264
|
+
`Load the translations in ${localesRel}/*.json from your own i18next setup.`);
|
|
265
|
+
return { configFileCreated, entryPointModified, userOwnsInit: false, warnings };
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const ours = existingIndex.find((f) => f !== null);
|
|
269
|
+
await writer.write(ours?.path ?? indexPath, buildLocalesIndex(locales, sourceLocale));
|
|
270
|
+
}
|
|
271
|
+
const icu = await (0, i18next_icu_1.detectI18nextIcu)(workspace);
|
|
272
|
+
const note = (0, i18next_icu_1.missingIcuNote)(icu);
|
|
273
|
+
if (note)
|
|
274
|
+
warnings.push(note);
|
|
275
|
+
if (icu.userOwnsInit) {
|
|
276
|
+
warnings.push(`This app sets up i18next itself, so Babelize didn't add an i18n config. ` +
|
|
277
|
+
`Add the bundled translations (\`import { resources } from './${path.posix.relative(configDir, localesRel) || '.'}'\`) to your i18next.init().`);
|
|
278
|
+
return { configFileCreated, entryPointModified, userOwnsInit: true, warnings };
|
|
279
|
+
}
|
|
280
|
+
// i18n config: written once, next to the locales.
|
|
281
|
+
const configBase = path.join(workspace.path, configDir, 'i18n');
|
|
282
|
+
const existingConfig = SOURCE_EXTENSIONS.map((e) => `${configBase}${e}`);
|
|
283
|
+
let configExists = false;
|
|
284
|
+
for (const p of existingConfig)
|
|
285
|
+
if (await (0, file_utils_1.pathExists)(p))
|
|
286
|
+
configExists = true;
|
|
287
|
+
if (!configExists) {
|
|
288
|
+
let localesImport = path.posix.relative(configDir.split(path.sep).join('/'), localesRel);
|
|
289
|
+
localesImport = localesImport.startsWith('.') ? localesImport : `./${localesImport}`;
|
|
290
|
+
await writer.write(`${configBase}${ext}`, buildReactNativeConfig({
|
|
291
|
+
sourceLocale,
|
|
292
|
+
localeModule: localeModule(pkg),
|
|
293
|
+
localesImport,
|
|
294
|
+
typescript,
|
|
295
|
+
}));
|
|
296
|
+
configFileCreated = true;
|
|
297
|
+
}
|
|
298
|
+
// Entry point: import the config before anything else.
|
|
299
|
+
let entry = null;
|
|
300
|
+
for (const candidate of entryPointCandidates(pkg).map((c) => path.posix.join(appDir, c))) {
|
|
301
|
+
if (await (0, file_utils_1.pathExists)(path.join(workspace.path, candidate))) {
|
|
302
|
+
entry = candidate;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (!entry) {
|
|
307
|
+
warnings.push(`Couldn't find the app's entry point. Add \`import './${path.posix.join(configDir, 'i18n')}';\` ` +
|
|
308
|
+
`as the first line of it (app/_layout.tsx with Expo Router, otherwise index.js or App.tsx).`);
|
|
309
|
+
return { configFileCreated, entryPointModified, userOwnsInit: false, warnings };
|
|
310
|
+
}
|
|
311
|
+
const entryAbs = path.join(workspace.path, entry);
|
|
312
|
+
const content = await (0, file_utils_1.readFile)(entryAbs);
|
|
313
|
+
let specifier = path.posix.relative(path.posix.dirname(entry), path.posix.join(configDir.split(path.sep).join('/'), 'i18n'));
|
|
314
|
+
specifier = specifier.startsWith('.') ? specifier : `./${specifier}`;
|
|
315
|
+
if (!importsConfig(content, specifier)) {
|
|
316
|
+
// First line (after a leading comment block or directive): i18next must be
|
|
317
|
+
// initialized before other modules evaluate.
|
|
318
|
+
const directive = content.match(/^(?:\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*\n|(['"])use [a-z]+\1;?\s*\n))*/);
|
|
319
|
+
const at = directive ? directive[0].length : 0;
|
|
320
|
+
await writer.write(entryAbs, `${content.slice(0, at)}import '${specifier}';\n${content.slice(at)}`);
|
|
321
|
+
entryPointModified = true;
|
|
322
|
+
}
|
|
323
|
+
return { configFileCreated, entryPointModified, userOwnsInit: false, warnings };
|
|
324
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export declare const STATIC_RUNTIME_VERSION = "1.
|
|
2
|
-
export declare const RUNTIME_HEADER = "/* babelize:static-runtime v1.
|
|
1
|
+
export declare const STATIC_RUNTIME_VERSION = "1.1.0";
|
|
2
|
+
export declare const RUNTIME_HEADER = "/* babelize:static-runtime v1.1.0 */";
|
|
3
3
|
export declare function buildStaticRuntime(config: {
|
|
4
4
|
sourceLocale: string;
|
|
5
5
|
targetLocales: string[];
|
|
@@ -4,8 +4,9 @@ exports.RUNTIME_HEADER = exports.STATIC_RUNTIME_VERSION = void 0;
|
|
|
4
4
|
exports.buildStaticRuntime = buildStaticRuntime;
|
|
5
5
|
exports.buildSwitcherPlaceholder = buildSwitcherPlaceholder;
|
|
6
6
|
exports.checkRuntimeNeedsRegeneration = checkRuntimeNeedsRegeneration;
|
|
7
|
-
exports.STATIC_RUNTIME_VERSION = '1.
|
|
8
|
-
|
|
7
|
+
exports.STATIC_RUNTIME_VERSION = '1.1.0';
|
|
8
|
+
const RUNTIME_MARKER = '/* babelize:static-runtime v';
|
|
9
|
+
exports.RUNTIME_HEADER = `${RUNTIME_MARKER}${exports.STATIC_RUNTIME_VERSION} */`;
|
|
9
10
|
function buildStaticRuntime(config) {
|
|
10
11
|
const locales = JSON.stringify([config.sourceLocale, ...config.targetLocales]);
|
|
11
12
|
const baseDir = config.baseDir ? JSON.stringify(config.baseDir) : 'null';
|
|
@@ -113,8 +114,9 @@ function buildStaticRuntime(config) {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
function applyTranslations() {
|
|
117
|
+
// The whole document, so <title> and SEO <meta> tags in <head> are translated too.
|
|
116
118
|
var walk = document.createTreeWalker(
|
|
117
|
-
document.
|
|
119
|
+
document.documentElement,
|
|
118
120
|
NodeFilter.SHOW_ELEMENT,
|
|
119
121
|
null,
|
|
120
122
|
false
|
|
@@ -150,7 +152,7 @@ function buildStaticRuntime(config) {
|
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
|
-
var attrNames = ['alt', 'title', 'placeholder', 'aria-label', 'aria-description'];
|
|
155
|
+
var attrNames = ['alt', 'title', 'placeholder', 'aria-label', 'aria-description', 'content'];
|
|
154
156
|
for (var a = 0; a < attrNames.length; a++) {
|
|
155
157
|
var attrKey = node.getAttribute('data-i18n-' + attrNames[a]);
|
|
156
158
|
if (attrKey) {
|
|
@@ -162,6 +164,8 @@ function buildStaticRuntime(config) {
|
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
function updateHtmlAttributes() {
|
|
167
|
+
var select = document.querySelector('[data-babelize-switcher-select]');
|
|
168
|
+
if (select) select.value = lang;
|
|
165
169
|
var meta = (loadedLocales[lang] || {})._meta;
|
|
166
170
|
if (!meta) return;
|
|
167
171
|
var html = document.documentElement;
|
|
@@ -254,7 +258,8 @@ function checkRuntimeNeedsRegeneration(existingContent) {
|
|
|
254
258
|
if (!existingContent || existingContent.trim().length === 0) {
|
|
255
259
|
return 'missing';
|
|
256
260
|
}
|
|
257
|
-
|
|
261
|
+
// Any version we generated, so older runtimes get upgraded rather than treated as hand-written.
|
|
262
|
+
if (existingContent.includes(RUNTIME_MARKER)) {
|
|
258
263
|
return 'ours';
|
|
259
264
|
}
|
|
260
265
|
return 'user';
|