@babelize/engine 0.1.0 → 0.1.3

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 (104) hide show
  1. package/dist/analysis/analyze.js +1 -1
  2. package/dist/detection/stack.d.ts +42 -0
  3. package/dist/detection/stack.js +277 -0
  4. package/dist/django/catalog.d.ts +92 -0
  5. package/dist/django/catalog.js +677 -0
  6. package/dist/django/extract.d.ts +20 -0
  7. package/dist/django/extract.js +79 -0
  8. package/dist/django/inject-file.d.ts +6 -0
  9. package/dist/django/inject-file.js +52 -0
  10. package/dist/django/inject.d.ts +11 -0
  11. package/dist/django/inject.js +205 -0
  12. package/dist/django/keys.d.ts +21 -0
  13. package/dist/django/keys.js +63 -0
  14. package/dist/django/locale-names.d.ts +13 -0
  15. package/dist/django/locale-names.js +60 -0
  16. package/dist/django/mo-file.d.ts +6 -0
  17. package/dist/django/mo-file.js +60 -0
  18. package/dist/django/plural-forms.d.ts +34 -0
  19. package/dist/django/plural-forms.js +216 -0
  20. package/dist/django/po-file.d.ts +38 -0
  21. package/dist/django/po-file.js +273 -0
  22. package/dist/django/python-extract.d.ts +34 -0
  23. package/dist/django/python-extract.js +619 -0
  24. package/dist/django/python-lexer.d.ts +65 -0
  25. package/dist/django/python-lexer.js +398 -0
  26. package/dist/django/template-extract.d.ts +32 -0
  27. package/dist/django/template-extract.js +680 -0
  28. package/dist/django/types.d.ts +49 -0
  29. package/dist/django/types.js +15 -0
  30. package/dist/index.d.ts +5 -0
  31. package/dist/index.js +16 -2
  32. package/dist/local/pseudo-provider.js +5 -4
  33. package/dist/local/translate-core.d.ts +5 -2
  34. package/dist/local/translate-core.js +43 -5
  35. package/dist/local/translate-messages.js +7 -9
  36. package/dist/modules/extractor/extract.js +22 -14
  37. package/dist/modules/finalization/django-finalize.d.ts +15 -0
  38. package/dist/modules/finalization/django-finalize.js +156 -19
  39. package/dist/modules/finalization/finalize.d.ts +2 -0
  40. package/dist/modules/finalization/finalize.js +58 -30
  41. package/dist/modules/finalization/nextjs-finalize.d.ts +4 -1
  42. package/dist/modules/finalization/nextjs-finalize.js +8 -9
  43. package/dist/modules/finalization/writes.d.ts +29 -0
  44. package/dist/modules/finalization/writes.js +78 -0
  45. package/dist/modules/format-injection/index.js +3 -2
  46. package/dist/modules/glossary/index.d.ts +1 -2
  47. package/dist/modules/glossary/index.js +14 -6
  48. package/dist/modules/glossary/rules.d.ts +81 -0
  49. package/dist/modules/glossary/rules.js +244 -0
  50. package/dist/modules/injection/inject.js +14 -30
  51. package/dist/modules/keygen/keygen.js +7 -0
  52. package/dist/modules/locale-builder/locales.js +23 -0
  53. package/dist/modules/lockfile/index.d.ts +1 -1
  54. package/dist/modules/lockfile/index.js +2 -1
  55. package/dist/modules/lockfile/lockfile-manager.d.ts +9 -0
  56. package/dist/modules/lockfile/lockfile-manager.js +16 -1
  57. package/dist/modules/lockfile/schema.d.ts +42 -0
  58. package/dist/modules/lockfile/schema.js +11 -0
  59. package/dist/modules/translator/icu-branches.d.ts +138 -0
  60. package/dist/modules/translator/icu-branches.js +393 -0
  61. package/dist/modules/translator/translate.d.ts +40 -4
  62. package/dist/modules/translator/translate.js +297 -173
  63. package/dist/modules/workspace/validate.js +67 -32
  64. package/dist/orchestrator.d.ts +5 -0
  65. package/dist/orchestrator.js +125 -14
  66. package/dist/providers/types.d.ts +10 -2
  67. package/dist/qa/check-project.d.ts +2 -1
  68. package/dist/qa/check-project.js +50 -7
  69. package/dist/qa/gettext-files.d.ts +15 -0
  70. package/dist/qa/gettext-files.js +115 -0
  71. package/dist/qa/locale-qa.js +20 -3
  72. package/dist/runtime/config.js +1 -0
  73. package/dist/runtime/types.d.ts +64 -0
  74. package/dist/utils/extraction-options.d.ts +9 -3
  75. package/dist/utils/extraction-options.js +13 -3
  76. package/dist/utils/file-utils.js +7 -2
  77. package/dist/utils/icu-message.d.ts +2 -2
  78. package/dist/utils/icu-message.js +43 -7
  79. package/dist/utils/jsx-message.js +7 -0
  80. package/dist/utils/metadata.d.ts +4 -2
  81. package/dist/utils/metadata.js +16 -10
  82. package/dist/utils/not-injected.js +2 -0
  83. package/dist/utils/placeholder-protection.d.ts +18 -1
  84. package/dist/utils/placeholder-protection.js +58 -13
  85. package/dist/utils/translation-sanitizer.d.ts +1 -0
  86. package/dist/utils/translation-sanitizer.js +23 -9
  87. package/dist/utils/translator-load-balancer.js +5 -1
  88. package/dist/utils/untranslated.d.ts +15 -0
  89. package/dist/utils/untranslated.js +34 -0
  90. package/package.json +2 -6
  91. package/dist/injection/django/django-template-injector.d.ts +0 -29
  92. package/dist/injection/django/django-template-injector.js +0 -216
  93. package/dist/modules/glossary/glossary.d.ts +0 -9
  94. package/dist/modules/glossary/glossary.js +0 -27
  95. package/dist/modules/glossary/masker.d.ts +0 -23
  96. package/dist/modules/glossary/masker.js +0 -56
  97. package/dist/modules/locale-builder/po.d.ts +0 -33
  98. package/dist/modules/locale-builder/po.js +0 -131
  99. package/dist/modules/translator/translator.d.ts +0 -5
  100. package/dist/modules/translator/translator.js +0 -46
  101. package/dist/utils/dtl-mask.d.ts +0 -25
  102. package/dist/utils/dtl-mask.js +0 -58
  103. package/dist/utils/py-extractor.d.ts +0 -14
  104. package/dist/utils/py-extractor.js +0 -285
@@ -37,11 +37,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.resolveSettingsPath = resolveSettingsPath;
40
+ exports.resolveDjangoLocaleDir = resolveDjangoLocaleDir;
41
+ exports.parseLocalePaths = parseLocalePaths;
40
42
  exports.finalizeDjangoSettings = finalizeDjangoSettings;
41
43
  const path = __importStar(require("path"));
42
44
  const fast_glob_1 = __importDefault(require("fast-glob"));
43
45
  const file_utils_1 = require("../../utils/file-utils");
44
46
  const logger_1 = require("../../utils/logger");
47
+ const locale_names_1 = require("../../django/locale-names");
45
48
  /**
46
49
  * Django settings.py auto-configuration.
47
50
  *
@@ -134,7 +137,106 @@ async function resolveByGlob(workspacePath) {
134
137
  return path.join(workspacePath, shallowest);
135
138
  }
136
139
  async function resolveSettingsPath(workspacePath) {
137
- return (await resolveViaManagePy(workspacePath)) ?? (await resolveByGlob(workspacePath));
140
+ const resolved = (await resolveViaManagePy(workspacePath)) ?? (await resolveByGlob(workspacePath));
141
+ if (!resolved)
142
+ return null;
143
+ // settings/dev.py with `from .base import *`: the shared settings live in base.py,
144
+ // and editing only dev.py would leave production without i18n.
145
+ const source = await (0, file_utils_1.readFile)(resolved).catch(() => '');
146
+ if (!/^MIDDLEWARE\s*=/m.test(source)) {
147
+ for (const m of source.matchAll(/^from\s+\.(\w+)\s+import\s+\*/gm)) {
148
+ const sibling = path.join(path.dirname(resolved), `${m[1]}.py`);
149
+ const siblingSource = await (0, file_utils_1.readFile)(sibling).catch(() => '');
150
+ if (/^MIDDLEWARE\s*=/m.test(siblingSource))
151
+ return sibling;
152
+ }
153
+ }
154
+ return resolved;
155
+ }
156
+ /**
157
+ * The locale directory Django reads (LOCALE_PATHS[0]), relative to the
158
+ * workspace, '/'-separated. Falls back to <BASE_DIR>/locale — what finalize
159
+ * adds — when LOCALE_PATHS is absent or not a plain BASE_DIR path.
160
+ */
161
+ async function resolveDjangoLocaleDir(workspacePath, djangoRoot) {
162
+ const fallback = path.posix.join(djangoRoot, 'locale');
163
+ const settingsPath = await resolveSettingsPath(workspacePath);
164
+ if (!settingsPath)
165
+ return fallback;
166
+ const source = await (0, file_utils_1.readFile)(settingsPath).catch(() => '');
167
+ const rel = parseLocalePaths(source);
168
+ return rel ? path.posix.normalize(path.posix.join(djangoRoot, rel)) : fallback;
169
+ }
170
+ /** First LOCALE_PATHS entry as a path relative to BASE_DIR, if it is one. */
171
+ function parseLocalePaths(source) {
172
+ const m = /^LOCALE_PATHS\s*=\s*[[(]/m.exec(source);
173
+ if (!m)
174
+ return null;
175
+ // First element of the list: up to the first comma outside brackets.
176
+ let depth = 0;
177
+ let first = '';
178
+ for (let i = m.index + m[0].length; i < source.length; i++) {
179
+ const ch = source[i];
180
+ if (ch === '(' || ch === '[')
181
+ depth++;
182
+ else if (ch === ')' || ch === ']') {
183
+ if (depth === 0)
184
+ break;
185
+ depth--;
186
+ }
187
+ else if (ch === ',' && depth === 0)
188
+ break;
189
+ first += ch;
190
+ }
191
+ first = first.trim();
192
+ const strings = (t) => [...t.matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]);
193
+ // BASE_DIR / 'a' / 'b' str(BASE_DIR / 'a')
194
+ if (/^(?:str\()?\s*BASE_DIR\s*\//.test(first))
195
+ return path.posix.join(...strings(first));
196
+ // os.path.join(BASE_DIR, 'a', 'b') BASE_DIR.joinpath('a') Path(BASE_DIR, 'a')
197
+ if (/^(?:os\.path\.join|Path)\(\s*BASE_DIR\s*,/.test(first) || /^BASE_DIR\.joinpath\(/.test(first)) {
198
+ const parts = strings(first);
199
+ return parts.length ? path.posix.join(...parts) : null;
200
+ }
201
+ return null;
202
+ }
203
+ /** LANGUAGES entries for the source and target locales: ('pt-br', 'Brazilian Portuguese'). */
204
+ function languageEntries(locales) {
205
+ const seen = new Set();
206
+ const out = [];
207
+ for (const l of locales) {
208
+ const code = (0, locale_names_1.djangoLanguageCode)(l);
209
+ if (!l || seen.has(code))
210
+ continue;
211
+ seen.add(code);
212
+ out.push({ code, name: (0, locale_names_1.languageName)(l) });
213
+ }
214
+ return out;
215
+ }
216
+ /** Language codes listed in a LANGUAGES setting. */
217
+ function listedLanguages(source) {
218
+ const m = /^LANGUAGES\s*=\s*[[(]/m.exec(source);
219
+ if (!m)
220
+ return null;
221
+ let depth = 0;
222
+ let end = -1;
223
+ for (let i = m.index + m[0].length - 1; i < source.length; i++) {
224
+ const ch = source[i];
225
+ if (ch === '[' || ch === '(')
226
+ depth++;
227
+ else if (ch === ']' || ch === ')') {
228
+ depth--;
229
+ if (depth === 0) {
230
+ end = i + 1;
231
+ break;
232
+ }
233
+ }
234
+ }
235
+ if (end === -1)
236
+ return null;
237
+ const body = source.slice(m.index, end);
238
+ const codes = [...body.matchAll(/[[(,]\s*\(\s*['"]([\w-]+)['"]\s*,/g)].map((x) => x[1].toLowerCase());
239
+ return { codes, start: m.index, end };
138
240
  }
139
241
  /**
140
242
  * Insert LocaleMiddleware into the MIDDLEWARE list.
@@ -166,7 +268,7 @@ function insertLocaleMiddleware(source) {
166
268
  const entryLine = lines.find((l) => l.trim().startsWith("'") || l.trim().startsWith('"'));
167
269
  const indent = entryLine?.match(/^(\s*)/)?.[1] ?? ' ';
168
270
  const quote = entryLine?.trim().startsWith('"') ? '"' : "'";
169
- const newEntry = `${indent}${quote}${LOCALE_MIDDLEWARE}${quote},`;
271
+ const newEntry = `${indent}${quote}${LOCALE_MIDDLEWARE}${quote},${source.includes('\r\n') ? '\r' : ''}`;
170
272
  const sessionIdx = lines.findIndex((l) => l.includes(SESSION_MIDDLEWARE));
171
273
  const commonIdx = lines.findIndex((l) => l.includes(COMMON_MIDDLEWARE));
172
274
  let insertAt;
@@ -191,6 +293,8 @@ function insertLocaleMiddleware(source) {
191
293
  }
192
294
  async function finalizeDjangoSettings(workspacePath, options = {}) {
193
295
  const { targetLocales = [], sourceLocale = 'en', dryRun = false } = options;
296
+ const djangoRoot = options.djangoRoot ?? '.';
297
+ const localeRel = path.posix.relative(djangoRoot === '.' ? '' : djangoRoot, options.localeDir ?? path.posix.join(djangoRoot, 'locale')) || 'locale';
194
298
  const changes = [];
195
299
  const warnings = [];
196
300
  const settingsPath = await resolveSettingsPath(workspacePath);
@@ -204,12 +308,13 @@ async function finalizeDjangoSettings(workspacePath, options = {}) {
204
308
  return { settingsPath, changes, warnings };
205
309
  }
206
310
  let source = original;
311
+ const nl = original.includes('\r\n') ? '\r\n' : '\n';
207
312
  // ── USE_I18N ────────────────────────────────────────────────
208
313
  // Only flip an explicit False. Anything else the user wrote stays.
209
314
  if (hasSetting(source, 'USE_I18N')) {
210
- const falseMatch = source.match(/^USE_I18N\s*=\s*False\s*$/m);
211
- if (falseMatch) {
212
- source = source.replace(/^USE_I18N\s*=\s*False\s*$/m, 'USE_I18N = True');
315
+ const falseRe = /^USE_I18N[ \t]*=[ \t]*False\b/m;
316
+ if (falseRe.test(source)) {
317
+ source = source.replace(falseRe, 'USE_I18N = True');
213
318
  changes.push('Set USE_I18N = True (was False)');
214
319
  }
215
320
  }
@@ -227,24 +332,56 @@ async function finalizeDjangoSettings(workspacePath, options = {}) {
227
332
  // ── LOCALE_PATHS ────────────────────────────────────────────
228
333
  // Must match where the locale builder writes: <workspace>/locale/<lang>/LC_MESSAGES/
229
334
  if (!hasSetting(source, 'LOCALE_PATHS')) {
335
+ const segments = localeRel.split('/').filter(Boolean);
230
336
  const value = usesPathlibBaseDir(source)
231
- ? "LOCALE_PATHS = [BASE_DIR / 'locale']"
232
- : "LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]";
233
- source = appendSetting(source, value);
337
+ ? `LOCALE_PATHS = [BASE_DIR / ${segments.map((p) => `'${p}'`).join(' / ')}]`
338
+ : `LOCALE_PATHS = [os.path.join(BASE_DIR, ${segments.map((p) => `'${p}'`).join(', ')})]`;
339
+ source = appendSetting(source, value, nl);
234
340
  changes.push('Added LOCALE_PATHS');
235
341
  }
342
+ else {
343
+ const configured = parseLocalePaths(source);
344
+ if (configured === null) {
345
+ warnings.push(`LOCALE_PATHS in ${path.basename(settingsPath)} isn't a plain BASE_DIR path — make sure it includes ${localeRel}/ (where the .po files were written).`);
346
+ }
347
+ }
236
348
  // ── LANGUAGE_CODE ───────────────────────────────────────────
237
349
  // Only added when missing; an existing value is a deliberate choice.
238
350
  if (!hasSetting(source, 'LANGUAGE_CODE')) {
239
- source = appendSetting(source, `LANGUAGE_CODE = '${sourceLocale}'`);
240
- changes.push(`Added LANGUAGE_CODE = '${sourceLocale}'`);
351
+ source = appendSetting(source, `LANGUAGE_CODE = '${(0, locale_names_1.djangoLanguageCode)(sourceLocale)}'`, nl);
352
+ changes.push(`Added LANGUAGE_CODE = '${(0, locale_names_1.djangoLanguageCode)(sourceLocale)}'`);
241
353
  }
242
354
  // ── LANGUAGES ───────────────────────────────────────────────
243
- if (!hasSetting(source, 'LANGUAGES') && targetLocales.length > 0) {
244
- const all = [sourceLocale, ...targetLocales].filter((l, i, arr) => l && arr.indexOf(l) === i);
245
- const entries = all.map((l) => ` ('${l}', '${l}'),`).join('\n');
246
- source = appendSetting(source, `LANGUAGES = [\n${entries}\n]`);
247
- changes.push(`Added LANGUAGES (${all.length} locales)`);
355
+ // Django only activates languages listed here. Ours live in the Babelize
356
+ // block and grow with new target locales; a list the user wrote is theirs —
357
+ // we only point out what it is missing.
358
+ const wanted = languageEntries([sourceLocale, ...targetLocales]);
359
+ const listed = listedLanguages(source);
360
+ if (!listed && targetLocales.length > 0) {
361
+ const entries = wanted.map((e) => ` ('${e.code}', '${e.name.replace(/'/g, "\\'")}'),`).join(nl);
362
+ source = appendSetting(source, `LANGUAGES = [${nl}${entries}${nl}]`, nl);
363
+ changes.push(`Added LANGUAGES (${wanted.length} locales)`);
364
+ }
365
+ else if (listed) {
366
+ const missing = wanted.filter((e) => !listed.codes.includes(e.code) && !listed.codes.includes(e.code.split('-')[0]));
367
+ const managedAt = source.indexOf(MANAGED_BLOCK_HEADER);
368
+ if (missing.length > 0 && managedAt !== -1 && listed.start > managedAt) {
369
+ const body = source.slice(listed.start, listed.end);
370
+ const close = body.lastIndexOf(']');
371
+ if (close !== -1) {
372
+ const indent = /\n([ \t]+)\(/.exec(body)?.[1] ?? ' ';
373
+ const before = body.slice(0, close).replace(/\s*$/, '');
374
+ const sep = before.endsWith(',') || before.endsWith('[') ? '' : ',';
375
+ const added = missing.map((e) => `${indent}('${e.code}', '${e.name.replace(/'/g, "\\'")}'),`).join(nl);
376
+ const next = `${before}${sep}${nl}${added}${nl}${body.slice(close)}`;
377
+ source = source.slice(0, listed.start) + next + source.slice(listed.end);
378
+ changes.push(`Added ${missing.map((e) => e.code).join(', ')} to LANGUAGES`);
379
+ }
380
+ }
381
+ else if (missing.length > 0) {
382
+ warnings.push(`LANGUAGES in ${path.basename(settingsPath)} doesn't list ${missing.map((e) => `'${e.code}'`).join(', ')} — ` +
383
+ 'Django will not switch to those languages until you add them.');
384
+ }
248
385
  }
249
386
  if (changes.length === 0) {
250
387
  logger_1.engineLogger.info('Django settings already configured for i18n', { settingsPath });
@@ -258,7 +395,7 @@ async function finalizeDjangoSettings(workspacePath, options = {}) {
258
395
  changes,
259
396
  dryRun,
260
397
  });
261
- return { settingsPath, changes, warnings };
398
+ return { settingsPath, changes, warnings, original, updated: source };
262
399
  }
263
400
  /**
264
401
  * Append a setting under a single Babelize block at the end of the file.
@@ -267,9 +404,9 @@ async function finalizeDjangoSettings(workspacePath, options = {}) {
267
404
  * alternative, inserting near related settings, means moving code around a file
268
405
  * we cannot parse.
269
406
  */
270
- function appendSetting(source, statement) {
407
+ function appendSetting(source, statement, nl = '\n') {
271
408
  const withBlock = source.includes(MANAGED_BLOCK_HEADER)
272
409
  ? source
273
- : `${source.trimEnd()}\n\n${MANAGED_BLOCK_HEADER}\n`;
274
- return `${withBlock.trimEnd()}\n${statement}\n`;
410
+ : `${source.trimEnd()}${nl}${nl}${MANAGED_BLOCK_HEADER}${nl}`;
411
+ return `${withBlock.trimEnd()}${nl}${statement}${nl}`;
275
412
  }
@@ -1,4 +1,5 @@
1
1
  import type { WorkspaceInfo } from '../../runtime/types';
2
+ import { type FinalizationChange } from './writes';
2
3
  interface PackageJson {
3
4
  dependencies?: Record<string, string>;
4
5
  devDependencies?: Record<string, string>;
@@ -21,6 +22,7 @@ export declare function finalizeProject(workspace: WorkspaceInfo, dryRun: boolea
21
22
  entryPointModified: boolean;
22
23
  packagesInstalled: boolean;
23
24
  warnings: string[];
25
+ changedFiles: FinalizationChange[];
24
26
  };
25
27
  }>;
26
28
  /**
@@ -44,6 +44,7 @@ const ast_helpers_1 = require("../../utils/ast-helpers");
44
44
  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
+ const writes_1 = require("./writes");
47
48
  const i18next_icu_1 = require("../../utils/i18next-icu");
48
49
  const DEPENDENCIES_BY_FRAMEWORK = {
49
50
  react: {
@@ -235,6 +236,12 @@ async function finalizeProject(workspace, dryRun, options = {}) {
235
236
  let entryPointModified = false;
236
237
  let packagesInstalled = false;
237
238
  let userOwnsInit = false;
239
+ /**
240
+ * Every file finalization writes goes through this recorder, so hosts can
241
+ * ship all of them with the run (the PR, the ZIP, the CLI diff) — including
242
+ * files it creates.
243
+ */
244
+ const writer = new writes_1.FinalizationWriter(workspace.path, dryRun);
238
245
  // Coerced against FRAMEWORKS, not SUPPORTED_FRAMEWORKS. SUPPORTED_FRAMEWORKS
239
246
  // omits 'django', so a Django project used to collapse to 'unknown' here —
240
247
  // which made the two `framework === 'django'` branches below unreachable and
@@ -259,15 +266,19 @@ async function finalizeProject(workspace, dryRun, options = {}) {
259
266
  requiredDeps = nextjs_finalize_1.NEXT_INTL_DEPENDENCIES;
260
267
  const missingDeps = Object.keys(requiredDeps).filter((d) => !deps[d]);
261
268
  if (missingDeps.length > 0) {
269
+ pkg.dependencies = pkg.dependencies || {};
270
+ for (const dep of missingDeps) {
271
+ pkg.dependencies[dep] = requiredDeps[dep];
272
+ }
273
+ // Keep the user's key order (a sorted package.json is an unreviewable
274
+ // diff); only the dependency map is sorted, as npm does.
275
+ pkg.dependencies = Object.fromEntries(Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b)));
276
+ await writer.write(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
262
277
  if (!dryRun) {
263
- pkg.dependencies = pkg.dependencies || {};
264
- for (const dep of missingDeps) {
265
- pkg.dependencies[dep] = requiredDeps[dep];
266
- }
267
- // Keep the user's key order (a sorted package.json is an unreviewable
268
- // diff); only the dependency map is sorted, as npm does.
269
- pkg.dependencies = Object.fromEntries(Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b)));
270
- await (0, file_utils_1.writeJsonFile)(packageJsonPath, pkg, false);
278
+ // A committed lockfile must move with package.json, or `npm ci` /
279
+ // `--frozen-lockfile` fails on the merged PR. Snapshot it so the
280
+ // install's update is recorded (and shipped) like any other write.
281
+ const lockfilesBefore = await readLockfiles(workspace.path);
271
282
  if (options.installDependencies !== false) {
272
283
  try {
273
284
  const pkgManager = detectPackageManager(workspace.path);
@@ -297,6 +308,17 @@ async function finalizeProject(workspace, dryRun, options = {}) {
297
308
  warnings.push(`Added dependencies to package.json but auto-install failed. Please run your package manager to install: ${missingDeps.join(', ')}`);
298
309
  }
299
310
  }
311
+ const lockfilesAfter = await readLockfiles(workspace.path);
312
+ for (const [name, before] of lockfilesBefore) {
313
+ const after = lockfilesAfter.get(name);
314
+ if (after !== undefined && after !== before) {
315
+ writer.record(path.join(workspace.path, name), before, after);
316
+ }
317
+ else {
318
+ warnings.push(`${name} was not updated for the new dependencies (${missingDeps.join(', ')}). ` +
319
+ `Run your package manager's install and commit ${name} before merging, or frozen-lockfile installs (npm ci) will fail.`);
320
+ }
321
+ }
300
322
  }
301
323
  dependenciesInvalid = true;
302
324
  if (packagesInstalled) {
@@ -326,9 +348,7 @@ async function finalizeProject(workspace, dryRun, options = {}) {
326
348
  sourceLocale: workspace.sourceDirs[0] || 'en',
327
349
  targetLocales: workspace.existingLocales || [],
328
350
  });
329
- if (!dryRun) {
330
- await (0, file_utils_1.writeFile)(i18nJsPath, runtimeContent);
331
- }
351
+ await writer.write(i18nJsPath, runtimeContent);
332
352
  configFileCreated = true;
333
353
  if (regenerationStatus === 'ours') {
334
354
  warnings.push('Updated i18n.js (new version available).');
@@ -346,19 +366,24 @@ async function finalizeProject(workspace, dryRun, options = {}) {
346
366
  targetLocales: options.targetLocales,
347
367
  sourceLocale: options.sourceLocale,
348
368
  dryRun,
369
+ djangoRoot: workspace.djangoRoot,
370
+ localeDir: workspace.publicDir,
349
371
  });
350
372
  warnings.push(...django.warnings);
351
373
  if (django.changes.length > 0) {
352
374
  configFileCreated = true;
353
- warnings.push(`Configured ${path.basename(django.settingsPath ?? 'settings.py')}: ${django.changes.join('; ')}.` +
354
- ' Run `python manage.py compilemessages` to compile the .po files.');
375
+ warnings.push(`Configured ${path.basename(django.settingsPath ?? 'settings.py')}: ${django.changes.join('; ')}.`);
376
+ if (django.settingsPath && django.original !== undefined && django.updated !== undefined) {
377
+ // finalizeDjangoSettings writes settings.py itself (it needs the parse); record it here.
378
+ writer.record(django.settingsPath, django.original, django.updated);
379
+ }
355
380
  }
356
381
  }
357
382
  else if (useNextIntl) {
358
383
  const nextIntl = await (0, nextjs_finalize_1.finalizeNextIntl)(workspace, dryRun, {
359
384
  sourceLocale: options.sourceLocale,
360
385
  targetLocales: options.targetLocales,
361
- });
386
+ }, writer);
362
387
  warnings.push(...nextIntl.warnings);
363
388
  configFileCreated = configFileCreated || nextIntl.configFileCreated;
364
389
  entryPointModified = entryPointModified || nextIntl.entryPointModified;
@@ -375,8 +400,7 @@ async function finalizeProject(workspace, dryRun, options = {}) {
375
400
  const content = await (0, file_utils_1.readFile)(abs);
376
401
  const updated = (0, i18next_icu_1.addIcuToGeneratedConfig)(content);
377
402
  if (updated !== content) {
378
- if (!dryRun)
379
- await (0, file_utils_1.writeFile)(abs, updated);
403
+ await writer.write(abs, updated);
380
404
  warnings.push(`Added i18next-icu to ${rel} (needed for messages with placeholders and plurals).`);
381
405
  }
382
406
  }
@@ -391,15 +415,13 @@ async function finalizeProject(workspace, dryRun, options = {}) {
391
415
  (await (0, file_utils_1.pathExists)(path.join(workspace.path, 'tsconfig.json')));
392
416
  const targetPath = isTS ? i18nPath : i18nJsPath;
393
417
  const template = I18N_CONFIG_TEMPLATES[framework] || I18N_CONFIG_TEMPLATES['unknown'];
394
- if (!dryRun)
395
- await (0, file_utils_1.writeFile)(targetPath, template);
418
+ await writer.write(targetPath, template);
396
419
  configFileCreated = true;
397
420
  }
398
421
  if (framework === 'nextjs' && !userOwnsInit) {
399
422
  const providerPath = path.join(workspace.path, srcDir, 'I18nProvider.tsx');
400
423
  if (!(await (0, file_utils_1.pathExists)(providerPath))) {
401
- if (!dryRun)
402
- await (0, file_utils_1.writeFile)(providerPath, I18N_PROVIDER_TEMPLATE);
424
+ await writer.write(providerPath, I18N_PROVIDER_TEMPLATE);
403
425
  configFileCreated = true;
404
426
  }
405
427
  }
@@ -439,9 +461,7 @@ async function finalizeProject(workspace, dryRun, options = {}) {
439
461
  }
440
462
  const modified = modifyVueEntryPoint(content, relativeI18nPath);
441
463
  if (modified !== null) {
442
- if (!dryRun) {
443
- await (0, file_utils_1.writeFile)(entryPath, modified);
444
- }
464
+ await writer.write(entryPath, modified);
445
465
  entryPointModified = true;
446
466
  }
447
467
  break;
@@ -464,8 +484,7 @@ async function finalizeProject(workspace, dryRun, options = {}) {
464
484
  if (isNextLayout) {
465
485
  const modified = modifyNextjsLayout(content, relativeI18nPath);
466
486
  if (modified !== null) {
467
- if (!dryRun)
468
- await (0, file_utils_1.writeFile)(entryPath, modified);
487
+ await writer.write(entryPath, modified);
469
488
  entryPointModified = true;
470
489
  }
471
490
  break;
@@ -478,17 +497,15 @@ async function finalizeProject(workspace, dryRun, options = {}) {
478
497
  content.includes(`import "${relativeI18nPath}.ts"`) ||
479
498
  content.includes("import '@/i18n'");
480
499
  if (!hasImport) {
481
- if (!dryRun) {
482
- const newContent = `import '${relativeI18nPath}';\n${content}`;
483
- await (0, file_utils_1.writeFile)(entryPath, newContent);
484
- }
500
+ await writer.write(entryPath, `import '${relativeI18nPath}';\n${content}`);
485
501
  entryPointModified = true;
486
502
  }
487
503
  break;
488
504
  }
489
505
  }
490
506
  }
491
- return { finalization: { dependenciesInvalid, configFileCreated, entryPointModified, packagesInstalled, warnings } };
507
+ const changedFiles = writer.changes;
508
+ return { finalization: { dependenciesInvalid, configFileCreated, entryPointModified, packagesInstalled, warnings, changedFiles } };
492
509
  }
493
510
  /**
494
511
  * Wire Next.js App Router layouts through a client I18nProvider so i18n
@@ -687,6 +704,17 @@ function modifyVueEntryPoint(content, relativeI18nPath) {
687
704
  return `import i18n from '${relativeI18nPath}';\n${content}`;
688
705
  }
689
706
  }
707
+ /** Text lockfiles a package manager rewrites on install (bun.lockb is binary and left to the user). */
708
+ const TEXT_LOCKFILES = ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock'];
709
+ async function readLockfiles(workspacePath) {
710
+ const found = new Map();
711
+ for (const name of TEXT_LOCKFILES) {
712
+ const abs = path.join(workspacePath, name);
713
+ if (await (0, file_utils_1.pathExists)(abs))
714
+ found.set(name, await (0, file_utils_1.readFile)(abs));
715
+ }
716
+ return found;
717
+ }
690
718
  function detectPackageManager(workspacePath) {
691
719
  if ((0, fs_1.existsSync)(path.join(workspacePath, 'bun.lock')))
692
720
  return 'bun';
@@ -1,3 +1,4 @@
1
+ import { FinalizationWriter } from './writes';
1
2
  import type { WorkspaceInfo } from '../../runtime/types';
2
3
  /**
3
4
  * Next.js finalization for next-intl.
@@ -34,4 +35,6 @@ export declare function wrapRootLayout(content: string, typescript: boolean): st
34
35
  export declare function finalizeNextIntl(workspace: WorkspaceInfo, dryRun: boolean, options: {
35
36
  sourceLocale?: string;
36
37
  targetLocales?: string[];
37
- }): Promise<NextIntlFinalizeResult>;
38
+ },
39
+ /** Records every write so it ships with the run (finalizeProject passes its own). */
40
+ writer?: FinalizationWriter): Promise<NextIntlFinalizeResult>;
@@ -41,6 +41,7 @@ exports.finalizeNextIntl = finalizeNextIntl;
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  const file_utils_1 = require("../../utils/file-utils");
44
+ const writes_1 = require("./writes");
44
45
  const ast_helpers_1 = require("../../utils/ast-helpers");
45
46
  /**
46
47
  * Next.js finalization for next-intl.
@@ -234,7 +235,9 @@ async function availableLocales(workspace, options) {
234
235
  const targets = (options.targetLocales ?? []).filter((l) => l !== source);
235
236
  return [...new Set([...targets, ...[...found].sort()])];
236
237
  }
237
- async function finalizeNextIntl(workspace, dryRun, options) {
238
+ async function finalizeNextIntl(workspace, dryRun, options,
239
+ /** Records every write so it ships with the run (finalizeProject passes its own). */
240
+ writer = new writes_1.FinalizationWriter(workspace.path, dryRun)) {
238
241
  const warnings = [];
239
242
  let configFileCreated = false;
240
243
  let entryPointModified = false;
@@ -278,8 +281,7 @@ async function finalizeNextIntl(workspace, dryRun, options) {
278
281
  typescript: requestIsTs,
279
282
  });
280
283
  if (content !== existingContent) {
281
- if (!dryRun)
282
- await (0, file_utils_1.writeFile)(requestFile, content);
284
+ await writer.write(requestFile, content);
283
285
  configFileCreated = true;
284
286
  }
285
287
  }
@@ -290,8 +292,7 @@ async function finalizeNextIntl(workspace, dryRun, options) {
290
292
  const created = `import createNextIntlPlugin from 'next-intl/plugin';\n\n` +
291
293
  `const withNextIntl = createNextIntlPlugin(${JSON.stringify(pluginPath)});\n\n` +
292
294
  `/** @type {import('next').NextConfig} */\nconst nextConfig = {};\n\nexport default withNextIntl(nextConfig);\n`;
293
- if (!dryRun)
294
- await (0, file_utils_1.writeFile)(path.join(root, 'next.config.mjs'), created);
295
+ await writer.write(path.join(root, 'next.config.mjs'), created);
295
296
  configFileCreated = true;
296
297
  }
297
298
  else {
@@ -302,8 +303,7 @@ async function finalizeNextIntl(workspace, dryRun, options) {
302
303
  `createNextIntlPlugin(${JSON.stringify(pluginPath)}) from 'next-intl/plugin'.`);
303
304
  }
304
305
  else if (wrapped !== content) {
305
- if (!dryRun)
306
- await (0, file_utils_1.writeFile)(nextConfigPath, wrapped);
306
+ await writer.write(nextConfigPath, wrapped);
307
307
  configFileCreated = true;
308
308
  }
309
309
  }
@@ -320,8 +320,7 @@ async function finalizeNextIntl(workspace, dryRun, options) {
320
320
  'wrap it in <NextIntlClientProvider> from next-intl so Client Components can translate.');
321
321
  }
322
322
  else if (wrapped !== content) {
323
- if (!dryRun)
324
- await (0, file_utils_1.writeFile)(layoutPath, wrapped);
323
+ await writer.write(layoutPath, wrapped);
325
324
  entryPointModified = true;
326
325
  }
327
326
  }
@@ -0,0 +1,29 @@
1
+ /** A file finalization created or edited, relative to the workspace root (POSIX separators). */
2
+ export interface FinalizationChange {
3
+ file: string;
4
+ /** '' when the file did not exist before the run. */
5
+ originalContent: string;
6
+ modifiedContent: string;
7
+ /** True when finalization created the file (it was not in the project before). */
8
+ created: boolean;
9
+ }
10
+ /**
11
+ * The single path every finalization write goes through.
12
+ *
13
+ * Hosts ship `changes` with the run (the GitHub PR, the ZIP, the CLI report),
14
+ * so a write that bypasses this recorder is a file the PR silently leaves out:
15
+ * `t()` calls merged without the dependency or the init code that makes them
16
+ * work. Records new files as well as edits, and folds repeated writes to one
17
+ * file into a single change (first original, last content).
18
+ */
19
+ export declare class FinalizationWriter {
20
+ readonly root: string;
21
+ readonly dryRun: boolean;
22
+ private readonly byFile;
23
+ constructor(root: string, dryRun: boolean);
24
+ /** Write `content` to `absPath` (unless dry run) and record it. Returns false when the file already had that content. */
25
+ write(absPath: string, content: string): Promise<boolean>;
26
+ /** Record a write made elsewhere (e.g. by a package manager). `original` null = file was created. */
27
+ record(absPath: string, original: string | null, content: string): void;
28
+ get changes(): FinalizationChange[];
29
+ }
@@ -0,0 +1,78 @@
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.FinalizationWriter = void 0;
37
+ const path = __importStar(require("path"));
38
+ const file_utils_1 = require("../../utils/file-utils");
39
+ /**
40
+ * The single path every finalization write goes through.
41
+ *
42
+ * Hosts ship `changes` with the run (the GitHub PR, the ZIP, the CLI report),
43
+ * so a write that bypasses this recorder is a file the PR silently leaves out:
44
+ * `t()` calls merged without the dependency or the init code that makes them
45
+ * work. Records new files as well as edits, and folds repeated writes to one
46
+ * file into a single change (first original, last content).
47
+ */
48
+ class FinalizationWriter {
49
+ constructor(root, dryRun) {
50
+ this.root = root;
51
+ this.dryRun = dryRun;
52
+ this.byFile = new Map();
53
+ }
54
+ /** Write `content` to `absPath` (unless dry run) and record it. Returns false when the file already had that content. */
55
+ async write(absPath, content) {
56
+ const original = (await (0, file_utils_1.pathExists)(absPath)) ? await (0, file_utils_1.readFile)(absPath) : null;
57
+ if (original === content)
58
+ return false;
59
+ if (!this.dryRun)
60
+ await (0, file_utils_1.writeFile)(absPath, content);
61
+ this.record(absPath, original, content);
62
+ return true;
63
+ }
64
+ /** Record a write made elsewhere (e.g. by a package manager). `original` null = file was created. */
65
+ record(absPath, original, content) {
66
+ const file = path.relative(this.root, path.resolve(absPath)).split(path.sep).join('/');
67
+ const prev = this.byFile.get(file);
68
+ if (prev) {
69
+ prev.modifiedContent = content;
70
+ return;
71
+ }
72
+ this.byFile.set(file, { file, originalContent: original ?? '', modifiedContent: content, created: original === null });
73
+ }
74
+ get changes() {
75
+ return [...this.byFile.values()];
76
+ }
77
+ }
78
+ exports.FinalizationWriter = FinalizationWriter;
@@ -203,7 +203,8 @@ async function injectFormats(workspace, formatStrings, formatConfig, dryRun = fa
203
203
  if (!fileStrings || fileStrings.length === 0)
204
204
  continue;
205
205
  try {
206
- let content = await (0, file_utils_1.readFile)(filePath);
206
+ const originalContent = await (0, file_utils_1.readFile)(filePath);
207
+ let content = originalContent;
207
208
  const injections = [];
208
209
  for (const formatStr of fileStrings) {
209
210
  if (!formatStr.detectedFormat)
@@ -234,7 +235,7 @@ async function injectFormats(workspace, formatStrings, formatConfig, dryRun = fa
234
235
  await (0, file_utils_1.writeFile)(filePath, content);
235
236
  modifiedFiles.push({
236
237
  file: relativePath,
237
- originalContent: await (0, file_utils_1.readFile)(filePath).catch(() => ''),
238
+ originalContent,
238
239
  modifiedContent: content,
239
240
  injections,
240
241
  importAdded: false,