@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.
Files changed (55) hide show
  1. package/dist/analysis/analyze.js +1 -1
  2. package/dist/config.d.ts +3 -3
  3. package/dist/detection/stack.d.ts +3 -1
  4. package/dist/detection/stack.js +13 -6
  5. package/dist/injection/StrategyFactory.d.ts +5 -1
  6. package/dist/injection/StrategyFactory.js +7 -2
  7. package/dist/injection/html/html-injector.d.ts +2 -0
  8. package/dist/injection/html/html-injector.js +55 -6
  9. package/dist/injection/strategies/VanillaStrategy.d.ts +8 -0
  10. package/dist/injection/strategies/VanillaStrategy.js +10 -3
  11. package/dist/local/translate-core.d.ts +2 -2
  12. package/dist/local/translate-core.js +2 -2
  13. package/dist/modules/extractor/extract.js +67 -10
  14. package/dist/modules/finalization/dependencies.d.ts +10 -0
  15. package/dist/modules/finalization/dependencies.js +23 -0
  16. package/dist/modules/finalization/finalize.js +47 -43
  17. package/dist/modules/finalization/nextjs-finalize.js +2 -1
  18. package/dist/modules/finalization/react-native-finalize.d.ts +45 -0
  19. package/dist/modules/finalization/react-native-finalize.js +324 -0
  20. package/dist/modules/finalization/static-runtime.d.ts +2 -2
  21. package/dist/modules/finalization/static-runtime.js +10 -5
  22. package/dist/modules/format-injection/index.js +38 -9
  23. package/dist/modules/format-injection/inject-formats.d.ts +3 -2
  24. package/dist/modules/format-injection/inject-formats.js +4 -3
  25. package/dist/modules/injection/inject.d.ts +14 -1
  26. package/dist/modules/injection/inject.js +307 -154
  27. package/dist/modules/sync-detector/sync-detector.js +11 -4
  28. package/dist/modules/translator/line-breaks.d.ts +8 -0
  29. package/dist/modules/translator/line-breaks.js +63 -0
  30. package/dist/modules/translator/translate.js +5 -2
  31. package/dist/modules/workspace/validate.d.ts +13 -1
  32. package/dist/modules/workspace/validate.js +95 -58
  33. package/dist/runtime/types.d.ts +22 -0
  34. package/dist/runtime/types.js +33 -0
  35. package/dist/utils/ast-helpers.d.ts +30 -2
  36. package/dist/utils/ast-helpers.js +355 -42
  37. package/dist/utils/extraction-options.js +1 -1
  38. package/dist/utils/file-utils.js +4 -1
  39. package/dist/utils/i18n-config-path.d.ts +18 -0
  40. package/dist/utils/i18n-config-path.js +64 -0
  41. package/dist/utils/i18next-icu.js +1 -1
  42. package/dist/utils/jsx-message.d.ts +16 -1
  43. package/dist/utils/jsx-message.js +89 -9
  44. package/dist/utils/key-usage.js +1 -1
  45. package/dist/utils/metadata.d.ts +5 -1
  46. package/dist/utils/metadata.js +19 -1
  47. package/dist/utils/module-constants.d.ts +21 -3
  48. package/dist/utils/module-constants.js +123 -25
  49. package/dist/utils/react-native.d.ts +17 -0
  50. package/dist/utils/react-native.js +108 -0
  51. package/dist/utils/string-utils.d.ts +8 -0
  52. package/dist/utils/string-utils.js +29 -1
  53. package/dist/utils/ui-copy.d.ts +5 -4
  54. package/dist/utils/ui-copy.js +10 -5
  55. 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
- debug: process.env.NODE_ENV !== 'production',
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: process.env.NODE_ENV !== 'production',
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
- const packageJsonPath = path.join(workspace.path, 'package.json');
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 = workspace.sourceDirs.find((d) => d.includes('src')) || workspace.sourceDirs[0] || '.';
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
- const runtimeContent = (0, static_runtime_1.buildStaticRuntime)({
348
- sourceLocale: workspace.sourceDirs[0] || 'en',
349
- targetLocales: workspace.existingLocales || [],
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 : DEPENDENCIES_BY_FRAMEWORK[fw]);
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
- const root = workspace.path;
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.0.0";
2
- export declare const RUNTIME_HEADER = "/* babelize:static-runtime v1.0.0 */";
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.0.0';
8
- exports.RUNTIME_HEADER = `/* babelize:static-runtime v${exports.STATIC_RUNTIME_VERSION} */`;
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.body,
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
- if (existingContent.includes(exports.RUNTIME_HEADER)) {
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';