@babelize/engine 0.2.0 → 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.
@@ -139,7 +139,7 @@ async function analyzeWorkspace(workspacePath, options = {}) {
139
139
  const framework = workspace.framework;
140
140
  const support = frameworkSupport(framework);
141
141
  if (support === 'unsupported') {
142
- return empty(framework, `Detected ${framework}, which local translation does not support. Supported: React, Next.js and Django (Vue and static HTML in beta).`);
142
+ return empty(framework, `Detected ${framework}, which local translation does not support. Supported: React, Next.js, Django and static HTML (Vue and React Native in beta).`);
143
143
  }
144
144
  const phases = (types_1.FRAMEWORK_CONFIGS[framework] ?? types_1.FRAMEWORK_CONFIGS.unknown).phases;
145
145
  // Same ICU decision as a local translate run (whose translator is an LLM).
@@ -7,5 +7,7 @@ interface HtmlInjectOptions {
7
7
  targetLocales?: string[];
8
8
  sourceLocale?: string;
9
9
  }
10
+ /** `<script src>` for the root i18n.js as seen from this page: `i18n.js`, `../i18n.js`, ... */
11
+ export declare function runtimeScriptSrc(filePath: string): string;
10
12
  export declare function injectHtml(options: HtmlInjectOptions): Promise<FileInjection>;
11
13
  export {};
@@ -32,10 +32,36 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.runtimeScriptSrc = runtimeScriptSrc;
36
40
  exports.injectHtml = injectHtml;
41
+ const path_1 = __importDefault(require("path"));
37
42
  const string_utils_1 = require("../../utils/string-utils");
38
43
  const html_extractor_1 = require("../../utils/html-extractor");
44
+ // <meta> tags whose content the extractor collects (html-extractor extractHeadStrings).
45
+ const SEO_META = ['description', 'og:title', 'og:description', 'og:site_name', 'twitter:title', 'twitter:description'];
46
+ /**
47
+ * Key for a string in this file found by its text alone. Head strings are extracted
48
+ * with ids like `file:meta:<hash>`, and a string already seen in the body is not
49
+ * extracted twice, so a line-based id can't find them.
50
+ */
51
+ function keyByText(filePath, text, keyMapping) {
52
+ const prefix = `${filePath}:`;
53
+ const suffix = `:${(0, string_utils_1.hashString)(text)}`;
54
+ for (const [id, key] of keyMapping) {
55
+ if (id.startsWith(prefix) && id.endsWith(suffix))
56
+ return key;
57
+ }
58
+ return undefined;
59
+ }
60
+ /** `<script src>` for the root i18n.js as seen from this page: `i18n.js`, `../i18n.js`, ... */
61
+ function runtimeScriptSrc(filePath) {
62
+ const dir = path_1.default.posix.dirname(filePath.split(path_1.default.sep).join('/'));
63
+ return path_1.default.posix.relative(dir, 'i18n.js') || 'i18n.js';
64
+ }
39
65
  function asElement(node) {
40
66
  return node.nodeType === 1 ? node : null;
41
67
  }
@@ -60,13 +86,26 @@ function walkHtmlNode(node, filePath, content, keyMapping, matchedIds, injection
60
86
  }
61
87
  }
62
88
  const element = asElement(node);
89
+ if (element && tagName === 'meta') {
90
+ const name = element.getAttribute('name') || element.getAttribute('property');
91
+ const value = element.getAttribute('content');
92
+ if (name && SEO_META.includes(name) && value?.trim() && !element.getAttribute('data-i18n-content')) {
93
+ const key = keyByText(filePath, value.trim(), keyMapping);
94
+ if (key) {
95
+ element.setAttribute('data-i18n-content', key);
96
+ injections.push({ file: filePath, line: 0, column: 0, original: value, replacement: `data-i18n-content="${key}"`, key, confidence: 100 });
97
+ }
98
+ }
99
+ return;
100
+ }
63
101
  if (textChild && !nonTextChild && element && !element.getAttribute('data-i18n')) {
64
102
  const text = textChild.text.trim();
65
103
  if (node.range) {
66
104
  const line = (content.slice(0, node.range[0]).match(/\n/g) || []).length + 1;
67
105
  const id = `${filePath}:${line}:${(0, string_utils_1.hashString)(text)}`;
68
- if (matchedIds.has(id)) {
69
- const key = keyMapping.get(id);
106
+ // <title> may only exist under a head id (`file:title:<hash>`) or share a body string's key.
107
+ if (matchedIds.has(id) || tagName === 'title') {
108
+ const key = keyMapping.get(id) ?? (tagName === 'title' ? keyByText(filePath, text, keyMapping) : undefined);
70
109
  if (key) {
71
110
  element.setAttribute('data-i18n', key);
72
111
  injections.push({
@@ -159,13 +198,23 @@ async function injectHtml(options) {
159
198
  const matchedIds = new Set(keyMapping.keys());
160
199
  walkHtmlNode(htmlEl, filePath, content, keyMapping, matchedIds, injections);
161
200
  let modifiedContent = root.toString();
162
- const hasScript = /<script[^>]*src\s*=\s*["']i18n\.js["'][^>]*>/.test(modifiedContent);
163
- if (!hasScript) {
201
+ // i18n.js lives at the site root, so pages in subfolders need ../i18n.js. Older runs
202
+ // wrote a bare src="i18n.js" everywhere; repoint those instead of adding a second tag.
203
+ const src = runtimeScriptSrc(filePath);
204
+ const existingScript = /<script([^>]*)src\s*=\s*["']((?:\.\.\/)*i18n\.js)["']/.exec(modifiedContent);
205
+ if (existingScript) {
206
+ if (existingScript[2] !== src) {
207
+ modifiedContent = modifiedContent.replace(existingScript[0], `<script${existingScript[1]}src="${src}"`);
208
+ importAdded = true;
209
+ }
210
+ }
211
+ else {
212
+ const tag = ` <script src="${src}"></script>\n`;
164
213
  if (modifiedContent.includes('</body>')) {
165
- modifiedContent = modifiedContent.replace('</body>', ' <script src="i18n.js"></script>\n</body>');
214
+ modifiedContent = modifiedContent.replace('</body>', `${tag}</body>`);
166
215
  }
167
216
  else {
168
- modifiedContent = modifiedContent.replace('</head>', ' <script src="i18n.js"></script>\n</head>');
217
+ modifiedContent = modifiedContent.replace('</head>', `${tag}</head>`);
169
218
  }
170
219
  importAdded = true;
171
220
  }
@@ -10,8 +10,8 @@ import { type FileDiffStat, type ApplyIo, type WorkspaceChanges } from './worksp
10
10
  /** Lockfile scope for local runs: `.babelize/local/babelize-lock.json` in the project root. */
11
11
  export declare const LOCAL_PROJECT_ID = "local";
12
12
  export declare const LOCAL_LOCKFILE = ".babelize/local/babelize-lock.json";
13
- export declare const SUPPORTED_LOCAL_FRAMEWORKS: readonly ["react", "nextjs", "django"];
14
- export declare const BETA_LOCAL_FRAMEWORKS: readonly ["vue", "static", "react-native"];
13
+ export declare const SUPPORTED_LOCAL_FRAMEWORKS: readonly ["react", "nextjs", "django", "static"];
14
+ export declare const BETA_LOCAL_FRAMEWORKS: readonly ["vue", "react-native"];
15
15
  export interface LocalTranslateOptions {
16
16
  projectDir: string;
17
17
  sourceLocale: string;
@@ -72,8 +72,8 @@ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
72
72
  /** Lockfile scope for local runs: `.babelize/local/babelize-lock.json` in the project root. */
73
73
  exports.LOCAL_PROJECT_ID = 'local';
74
74
  exports.LOCAL_LOCKFILE = `.babelize/${exports.LOCAL_PROJECT_ID}/babelize-lock.json`;
75
- exports.SUPPORTED_LOCAL_FRAMEWORKS = ['react', 'nextjs', 'django'];
76
- exports.BETA_LOCAL_FRAMEWORKS = ['vue', 'static', 'react-native'];
75
+ exports.SUPPORTED_LOCAL_FRAMEWORKS = ['react', 'nextjs', 'django', 'static'];
76
+ exports.BETA_LOCAL_FRAMEWORKS = ['vue', 'react-native'];
77
77
  class LocalTranslateError extends Error {
78
78
  constructor(message, code,
79
79
  /** Files left changed in the project (only for a failed, un-rolled-back apply). */
@@ -334,10 +334,12 @@ async function finalizeProject(workspace, dryRun, options = {}) {
334
334
  }
335
335
  else {
336
336
  // 'missing' or 'ours' — write/overwrite
337
- const runtimeContent = (0, static_runtime_1.buildStaticRuntime)({
338
- sourceLocale: workspace.sourceDirs[0] || 'en',
339
- targetLocales: workspace.existingLocales || [],
340
- });
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 });
341
343
  await writer.write(i18nJsPath, runtimeContent);
342
344
  configFileCreated = true;
343
345
  if (regenerationStatus === 'ours') {
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelize/engine",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Babelize translation pipeline (extract, keygen, translate, inject). Infrastructure is injected through provider/store interfaces.",
5
5
  "keywords": [
6
6
  "babelize",