@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
|
@@ -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
|
+
}
|
|
@@ -41,7 +41,19 @@ const inject_formats_1 = require("./inject-formats");
|
|
|
41
41
|
const ast_helpers_1 = require("../../utils/ast-helpers");
|
|
42
42
|
const file_utils_1 = require("../../utils/file-utils");
|
|
43
43
|
const types_1 = require("../../runtime/types");
|
|
44
|
+
const StrategyFactory_1 = require("../../injection/StrategyFactory");
|
|
44
45
|
const generator_1 = require("@babel/generator");
|
|
46
|
+
/** The local name of `import X from 'i18next'` in a file, if it has one. */
|
|
47
|
+
function i18nextDefaultImport(ast) {
|
|
48
|
+
for (const node of ast.program.body) {
|
|
49
|
+
if (!t.isImportDeclaration(node) || node.source.value !== 'i18next')
|
|
50
|
+
continue;
|
|
51
|
+
const def = node.specifiers.find((s) => t.isImportDefaultSpecifier(s));
|
|
52
|
+
if (def)
|
|
53
|
+
return def.local.name;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
45
57
|
/**
|
|
46
58
|
* Run format injection for all format strings.
|
|
47
59
|
*
|
|
@@ -66,13 +78,20 @@ async function injectFormats(workspace, formatStrings, formatConfig, dryRun = fa
|
|
|
66
78
|
}
|
|
67
79
|
stringsByFile.get(fileKey).push(s);
|
|
68
80
|
}
|
|
69
|
-
//
|
|
70
|
-
|
|
81
|
+
// The locale the formatted values follow. react-i18next (React, React
|
|
82
|
+
// Native, Next.js with react-i18next): the app's current language,
|
|
83
|
+
// `i18next.language` — React Native has no navigator.language, and on the
|
|
84
|
+
// web it ignored the language the user picked. Before init it is undefined,
|
|
85
|
+
// which Intl reads as the device default. Elsewhere the browser's language:
|
|
86
|
+
// next-intl's useLocale() and vue-i18n's composable only work in components.
|
|
87
|
+
const deps = { ...workspace.packageJson?.dependencies, ...workspace.packageJson?.devDependencies };
|
|
88
|
+
const followsI18next = StrategyFactory_1.StrategyFactory.getStrategy(workspace.framework || 'unknown', deps).id === 'react-i18next';
|
|
89
|
+
const browserLocaleExpr = (0, inject_formats_1.buildLocaleExpression)('browser');
|
|
71
90
|
// Find all source files
|
|
72
91
|
const sourceFiles = [];
|
|
73
92
|
for (const sourceDir of workspace.sourceDirs) {
|
|
74
93
|
const dirPath = path.join(workspace.path, sourceDir);
|
|
75
|
-
const files = await (0, file_utils_1.findFiles)(dirPath, types_1.SUPPORTED_EXTENSIONS, types_1.
|
|
94
|
+
const files = await (0, file_utils_1.findFiles)(dirPath, types_1.SUPPORTED_EXTENSIONS, (0, types_1.workspaceExcludes)(workspace));
|
|
76
95
|
sourceFiles.push(...files);
|
|
77
96
|
}
|
|
78
97
|
// Build a lookup of format strings by (relative file path, line)
|
|
@@ -93,11 +112,17 @@ async function injectFormats(workspace, formatStrings, formatConfig, dryRun = fa
|
|
|
93
112
|
try {
|
|
94
113
|
const originalContent = await (0, file_utils_1.readFile)(filePath);
|
|
95
114
|
const isTypeScript = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
|
96
|
-
const ast = (0, ast_helpers_1.parseCode)(originalContent, { isTypeScript });
|
|
115
|
+
const ast = (0, ast_helpers_1.parseCode)(originalContent, { isTypeScript, preserveFormat: true });
|
|
97
116
|
const injections = [];
|
|
98
117
|
const lineLookup = formatStringLookup.get(relativePath);
|
|
99
118
|
if (!lineLookup)
|
|
100
119
|
continue;
|
|
120
|
+
const existingI18next = followsI18next ? i18nextDefaultImport(ast) : null;
|
|
121
|
+
// A name nothing else in the file binds (`import * as i18next from 'i18next'`, a local `i18next`).
|
|
122
|
+
const i18nextName = existingI18next ?? ((0, ast_helpers_1.bindsTopLevel)(ast, 'i18next') ? 'i18nextInstance' : 'i18next');
|
|
123
|
+
const fileLocaleExpr = followsI18next
|
|
124
|
+
? t.memberExpression(t.identifier(i18nextName), t.identifier('language'))
|
|
125
|
+
: browserLocaleExpr;
|
|
101
126
|
// Walk AST and replace matching string literals with Intl.* expressions
|
|
102
127
|
(0, ast_helpers_1.traverse)(ast, {
|
|
103
128
|
StringLiteral(strPath) {
|
|
@@ -111,7 +136,7 @@ async function injectFormats(workspace, formatStrings, formatConfig, dryRun = fa
|
|
|
111
136
|
// Verify the text matches exactly (avoid false positives from partial matches)
|
|
112
137
|
if (node.value !== formatStr.text)
|
|
113
138
|
return;
|
|
114
|
-
const astExpr = (0, inject_formats_1.buildFormatAstExpression)(formatStr.detectedFormat, formatConfig,
|
|
139
|
+
const astExpr = (0, inject_formats_1.buildFormatAstExpression)(formatStr.detectedFormat, formatConfig, t.cloneNode(fileLocaleExpr));
|
|
115
140
|
if (!astExpr) {
|
|
116
141
|
skippedFiles.push({
|
|
117
142
|
file: relativePath,
|
|
@@ -144,7 +169,7 @@ async function injectFormats(workspace, formatStrings, formatConfig, dryRun = fa
|
|
|
144
169
|
const text = node.value.trim();
|
|
145
170
|
if (!text)
|
|
146
171
|
return;
|
|
147
|
-
const astExpr = (0, inject_formats_1.buildFormatAstExpression)(formatStr.detectedFormat, formatConfig,
|
|
172
|
+
const astExpr = (0, inject_formats_1.buildFormatAstExpression)(formatStr.detectedFormat, formatConfig, t.cloneNode(fileLocaleExpr));
|
|
148
173
|
if (!astExpr) {
|
|
149
174
|
skippedFiles.push({
|
|
150
175
|
file: relativePath,
|
|
@@ -170,6 +195,10 @@ async function injectFormats(workspace, formatStrings, formatConfig, dryRun = fa
|
|
|
170
195
|
});
|
|
171
196
|
if (injections.length === 0)
|
|
172
197
|
continue;
|
|
198
|
+
const importAdded = followsI18next && !existingI18next;
|
|
199
|
+
if (importAdded) {
|
|
200
|
+
ast.program.body.unshift(t.importDeclaration([t.importDefaultSpecifier(t.identifier(i18nextName))], t.stringLiteral('i18next')));
|
|
201
|
+
}
|
|
173
202
|
const modifiedContent = (0, ast_helpers_1.generateCode)(ast, originalContent);
|
|
174
203
|
if (!dryRun) {
|
|
175
204
|
await (0, file_utils_1.writeFile)(filePath, modifiedContent);
|
|
@@ -179,7 +208,7 @@ async function injectFormats(workspace, formatStrings, formatConfig, dryRun = fa
|
|
|
179
208
|
originalContent,
|
|
180
209
|
modifiedContent,
|
|
181
210
|
injections,
|
|
182
|
-
importAdded
|
|
211
|
+
importAdded,
|
|
183
212
|
hookAdded: false,
|
|
184
213
|
});
|
|
185
214
|
totalInjections += injections.length;
|
|
@@ -196,7 +225,7 @@ async function injectFormats(workspace, formatStrings, formatConfig, dryRun = fa
|
|
|
196
225
|
const htmlExtensions = ['.html', '.htm'];
|
|
197
226
|
for (const sourceDir of workspace.sourceDirs) {
|
|
198
227
|
const dirPath = path.join(workspace.path, sourceDir);
|
|
199
|
-
const files = await (0, file_utils_1.findFiles)(dirPath, htmlExtensions, types_1.
|
|
228
|
+
const files = await (0, file_utils_1.findFiles)(dirPath, htmlExtensions, (0, types_1.workspaceExcludes)(workspace));
|
|
200
229
|
for (const filePath of files) {
|
|
201
230
|
const relativePath = (0, file_utils_1.getRelativePath)(workspace.path, filePath);
|
|
202
231
|
const fileStrings = stringsByFile.get(relativePath);
|
|
@@ -213,7 +242,7 @@ async function injectFormats(workspace, formatStrings, formatConfig, dryRun = fa
|
|
|
213
242
|
const regex = new RegExp(escaped, 'g');
|
|
214
243
|
if (!regex.test(content))
|
|
215
244
|
continue;
|
|
216
|
-
const astExpr = (0, inject_formats_1.buildFormatAstExpression)(formatStr.detectedFormat, formatConfig,
|
|
245
|
+
const astExpr = (0, inject_formats_1.buildFormatAstExpression)(formatStr.detectedFormat, formatConfig, browserLocaleExpr);
|
|
217
246
|
if (!astExpr)
|
|
218
247
|
continue;
|
|
219
248
|
const replacementCode = (0, generator_1.generate)(astExpr).code;
|
|
@@ -26,6 +26,7 @@ export declare function buildFormatInjection(extracted: ExtractedString, formatC
|
|
|
26
26
|
*/
|
|
27
27
|
export declare function buildFormatAstExpression(format: DetectedFormat, formatConfig: FormatLocaleConfig, localeExpr: t.Expression): t.Expression | null;
|
|
28
28
|
/**
|
|
29
|
-
* Build a locale expression
|
|
29
|
+
* Build a locale expression for an i18n library (an injection strategy id:
|
|
30
|
+
* 'react-i18next', 'next-intl'); anything else gets the browser's language.
|
|
30
31
|
*/
|
|
31
|
-
export declare function buildLocaleExpression(
|
|
32
|
+
export declare function buildLocaleExpression(library: string): t.Expression;
|
|
@@ -194,10 +194,11 @@ function buildNumberAstExpression(format, localeExpr) {
|
|
|
194
194
|
return t.callExpression(t.memberExpression(nfNew, t.identifier('format')), [t.numericLiteral(numericValue)]);
|
|
195
195
|
}
|
|
196
196
|
/**
|
|
197
|
-
* Build a locale expression
|
|
197
|
+
* Build a locale expression for an i18n library (an injection strategy id:
|
|
198
|
+
* 'react-i18next', 'next-intl'); anything else gets the browser's language.
|
|
198
199
|
*/
|
|
199
|
-
function buildLocaleExpression(
|
|
200
|
-
switch (
|
|
200
|
+
function buildLocaleExpression(library) {
|
|
201
|
+
switch (library) {
|
|
201
202
|
case 'react-i18next':
|
|
202
203
|
return t.memberExpression(t.identifier('i18n'), t.identifier('language'));
|
|
203
204
|
case 'next-intl':
|
|
@@ -14,7 +14,20 @@ positions?: Map<string, Set<number>>): Promise<InjectionResult>;
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function injectIntoFile(content: string, filePath: string, keyMapping: Map<string, string>, confidenceThreshold: number, framework: string, existingDeps?: Record<string, string>, forceTypeScript?: boolean,
|
|
16
16
|
/** extractionId → source offsets of the nodes extraction selected (see ExtractedString.offsets). */
|
|
17
|
-
positions?: Map<string, Set<number
|
|
17
|
+
positions?: Map<string, Set<number>>, options?: {
|
|
18
|
+
/**
|
|
19
|
+
* Import path (relative to this file) of the i18n config's module-level `t`.
|
|
20
|
+
* Set when Babelize owns the react-i18next config: strings outside any
|
|
21
|
+
* component the hook can go in use it. Unset, they are left as they are.
|
|
22
|
+
*/
|
|
23
|
+
moduleTranslator?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Messages with placeholders are formatted at runtime (ICU): template
|
|
26
|
+
* literals and `+` concatenations become `t(key, { name })`. Off, they
|
|
27
|
+
* are left as they are.
|
|
28
|
+
*/
|
|
29
|
+
placeholders?: boolean;
|
|
30
|
+
}): FileInjection;
|
|
18
31
|
/**
|
|
19
32
|
* Inject translations into a Vue SFC file.
|
|
20
33
|
* Uses @vue/compiler-sfc to parse, vue/template-scan to find translatable
|