@babelize/engine 0.1.2 → 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.
Files changed (52) hide show
  1. package/dist/config.d.ts +3 -3
  2. package/dist/detection/stack.d.ts +3 -1
  3. package/dist/detection/stack.js +13 -6
  4. package/dist/injection/StrategyFactory.d.ts +5 -1
  5. package/dist/injection/StrategyFactory.js +7 -2
  6. package/dist/injection/strategies/VanillaStrategy.d.ts +8 -0
  7. package/dist/injection/strategies/VanillaStrategy.js +10 -3
  8. package/dist/local/translate-core.d.ts +1 -1
  9. package/dist/local/translate-core.js +1 -1
  10. package/dist/modules/extractor/extract.js +67 -10
  11. package/dist/modules/finalization/dependencies.d.ts +10 -0
  12. package/dist/modules/finalization/dependencies.js +23 -0
  13. package/dist/modules/finalization/finalize.js +41 -39
  14. package/dist/modules/finalization/nextjs-finalize.js +2 -1
  15. package/dist/modules/finalization/react-native-finalize.d.ts +45 -0
  16. package/dist/modules/finalization/react-native-finalize.js +324 -0
  17. package/dist/modules/format-injection/index.js +38 -9
  18. package/dist/modules/format-injection/inject-formats.d.ts +3 -2
  19. package/dist/modules/format-injection/inject-formats.js +4 -3
  20. package/dist/modules/injection/inject.d.ts +14 -1
  21. package/dist/modules/injection/inject.js +307 -154
  22. package/dist/modules/sync-detector/sync-detector.js +11 -4
  23. package/dist/modules/translator/line-breaks.d.ts +8 -0
  24. package/dist/modules/translator/line-breaks.js +63 -0
  25. package/dist/modules/translator/translate.js +19 -7
  26. package/dist/modules/workspace/validate.d.ts +13 -1
  27. package/dist/modules/workspace/validate.js +95 -58
  28. package/dist/runtime/types.d.ts +22 -0
  29. package/dist/runtime/types.js +33 -0
  30. package/dist/utils/ast-helpers.d.ts +30 -2
  31. package/dist/utils/ast-helpers.js +355 -42
  32. package/dist/utils/extraction-options.js +1 -1
  33. package/dist/utils/file-utils.js +4 -1
  34. package/dist/utils/i18n-config-path.d.ts +18 -0
  35. package/dist/utils/i18n-config-path.js +64 -0
  36. package/dist/utils/i18next-icu.js +1 -1
  37. package/dist/utils/icu-message.d.ts +2 -2
  38. package/dist/utils/icu-message.js +43 -7
  39. package/dist/utils/jsx-message.d.ts +16 -1
  40. package/dist/utils/jsx-message.js +89 -9
  41. package/dist/utils/key-usage.js +1 -1
  42. package/dist/utils/metadata.d.ts +5 -1
  43. package/dist/utils/metadata.js +19 -1
  44. package/dist/utils/module-constants.d.ts +21 -3
  45. package/dist/utils/module-constants.js +123 -25
  46. package/dist/utils/react-native.d.ts +17 -0
  47. package/dist/utils/react-native.js +108 -0
  48. package/dist/utils/string-utils.d.ts +8 -0
  49. package/dist/utils/string-utils.js +29 -1
  50. package/dist/utils/ui-copy.d.ts +5 -4
  51. package/dist/utils/ui-copy.js +10 -5
  52. package/package.json +3 -6
@@ -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
- // Build locale expression based on framework
70
- const localeExpr = (0, inject_formats_1.buildLocaleExpression)(workspace.framework || 'unknown');
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.EXCLUDED_PATTERNS);
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, localeExpr);
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, localeExpr);
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: false,
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.EXCLUDED_PATTERNS);
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, localeExpr);
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 based on the detected framework.
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(framework: string): t.Expression;
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 based on the detected framework.
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(framework) {
200
- switch (framework) {
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>>): FileInjection;
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