@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
@@ -0,0 +1,49 @@
1
+ /** Shared types for the Django (gettext) pipeline. */
2
+ export interface GettextMessage {
3
+ msgid: string;
4
+ msgidPlural?: string;
5
+ msgctxt?: string;
6
+ /** PO flags such as python-format / python-brace-format. */
7
+ flags: string[];
8
+ /** Translator comments (`#.` in the PO file). */
9
+ comments: string[];
10
+ }
11
+ /** A hardcoded string the injector can wrap. */
12
+ export interface PythonEdit {
13
+ kind: 'python';
14
+ start: number;
15
+ end: number;
16
+ /** Evaluated at import time (module/class level): needs gettext_lazy. */
17
+ lazy: boolean;
18
+ /** Argument text placed inside the gettext call. */
19
+ inner: string;
20
+ /** Text appended after the call, e.g. ` % {"count": count}`. */
21
+ suffix: string;
22
+ }
23
+ export interface TemplateEdit {
24
+ kind: 'template';
25
+ start: number;
26
+ end: number;
27
+ replacement: string;
28
+ }
29
+ export interface DjangoFinding {
30
+ message: GettextMessage;
31
+ /** 1-based line of the string in the original file. */
32
+ line: number;
33
+ /** Already marked for translation in the code (gettext call, trans tag). */
34
+ wrapped: boolean;
35
+ /** How to mark a hardcoded string (absent for wrapped strings). */
36
+ edit?: PythonEdit | TemplateEdit;
37
+ }
38
+ export interface DjangoUnwired {
39
+ line: number;
40
+ text: string;
41
+ reason: string;
42
+ }
43
+ export interface DjangoFileAnalysis {
44
+ findings: DjangoFinding[];
45
+ /** User-facing text found but deliberately not wrapped, with why. */
46
+ unwired: DjangoUnwired[];
47
+ }
48
+ /** PO format flags for a message, the way xgettext would mark it. */
49
+ export declare function formatFlags(...texts: Array<string | undefined>): string[];
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ /** Shared types for the Django (gettext) pipeline. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.formatFlags = formatFlags;
5
+ const PYTHON_FORMAT = /%(?:\(\w+\))?[sdif]/;
6
+ const BRACE_FORMAT = /\{(?:\w+(?:\.\w+|\[\w+\])*)?(?:![rsa])?(?::[^{}]*)?\}/;
7
+ /** PO format flags for a message, the way xgettext would mark it. */
8
+ function formatFlags(...texts) {
9
+ const all = texts.filter((t) => !!t).map((t) => t.replace(/%%/g, ''));
10
+ if (all.some((t) => PYTHON_FORMAT.test(t)))
11
+ return ['python-format'];
12
+ if (all.some((t) => BRACE_FORMAT.test(t.replace(/\{\{|\}\}/g, ''))))
13
+ return ['python-brace-format'];
14
+ return [];
15
+ }
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export { BabelizeEngine } from './BabelizeEngine';
9
9
  export type { BabelizeConfig, BabelizeEngineDependencies, BabelRunResult, ExtractedString, TranslationResult, TranslationUnit, } from './types';
10
10
  export { loadBabelizeConfig } from './config';
11
11
  export { runEngineV2, validateEngineConfigV2 } from './runtime';
12
+ export { RunAbortedError } from './orchestrator';
12
13
  export type { EngineConfigV2, EngineResultV2 } from './runtime';
13
14
  export type { KeyUsageReport, ProcessingStats } from './runtime/types';
14
15
  export { scanKeyReferences } from './utils/key-usage';
@@ -25,6 +26,8 @@ export type { SyncDetectionResult, SyncDetectedString, SyncMissingLocaleKey } fr
25
26
  export type { TranslationEntry, WorkspaceInfo } from './runtime/types';
26
27
  export { addBabelToHistory, detectProjectInfo, getBabelizeSummary, getProjectInfo, saveCachedStrings, saveProjectConfig, } from './utils/metadata';
27
28
  export type { BabelHistoryEntry, ExtractedStringEntry, ProjectInfo } from './utils/metadata';
29
+ export { detectStack, localRepoFiles, FRAMEWORK_DISPLAY_NAMES } from './detection/stack';
30
+ export type { DetectedStack, RepoFiles, StackFramework, PackageManagerId } from './detection/stack';
28
31
  export { parseCode } from './utils/ast-helpers';
29
32
  export { extractPlaceholders, hasICUSyntax, hashString, protectPlaceholders, restorePlaceholders, } from './utils/string-utils';
30
33
  export { sanitizeTranslation, validateRawTokens } from './utils/translation-sanitizer';
@@ -43,6 +46,8 @@ export { localeExtension } from './utils/file-utils';
43
46
  export { analyzeProject, analyzeWorkspace, frameworkSupport, } from './analysis/analyze';
44
47
  export type { AnalyzeOptions, AnalyzedString, FrameworkSupport, ProjectAnalysis } from './analysis/analyze';
45
48
  export { checkLocalePair, flattenLocaleMessages, tallyLocaleIssues } from './qa/locale-qa';
49
+ export { canonicalLocale, canonicalizeLocaleKeys, containsTerm, localeValue, normalizeGlossary, glossaryFingerprint, } from './modules/glossary';
50
+ export type { GlossaryRules, GlossaryRuleInput } from './modules/glossary';
46
51
  export type { LocaleGlossary, LocaleIssue, LocaleIssueCode, LocaleIssueSeverity, LocalePairOptions, LocalePairReport, } from './qa/locale-qa';
47
52
  export { DEFAULT_LOCALE_IGNORE_DIRS, findLocaleFileSets, groupLocaleFiles, isIgnoredLocalePath, isLocaleCode, loadLocale } from './qa/locale-files';
48
53
  export type { FindLocaleOptions, LoadedLocale, LocaleFileSet, ProjectFileReader } from './qa/locale-files';
package/dist/index.js CHANGED
@@ -21,8 +21,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
21
21
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.tallyLocaleIssues = exports.flattenLocaleMessages = exports.checkLocalePair = exports.frameworkSupport = exports.analyzeWorkspace = exports.analyzeProject = exports.localeExtension = exports.getCircuitBreaker = exports.CircuitState = exports.CircuitBreaker = exports.validateBatchQuality = exports.validateTranslationQuality = exports.translatorLoadBalancer = exports.warmupTranslator = exports.checkTranslatorHealth = exports.callTranslator = exports.requiredPluralCategories = exports.parseIcu = exports.isIcuMessage = exports.compareIcuMessages = exports.sameProtectedSpans = exports.restoreText = exports.protectText = exports.hasNothingToTranslate = exports.checkTokens = exports.validateRawTokens = exports.sanitizeTranslation = exports.restorePlaceholders = exports.protectPlaceholders = exports.hashString = exports.hasICUSyntax = exports.extractPlaceholders = exports.parseCode = exports.saveProjectConfig = exports.saveCachedStrings = exports.getProjectInfo = exports.getBabelizeSummary = exports.detectProjectInfo = exports.addBabelToHistory = exports.formatSyncNotificationMessage = exports.detectSyncIssues = exports.matchFromMemory = exports.setEngineLogger = exports.runWithEngineLogger = exports.validateWorkspace = exports.scanKeyReferences = exports.validateEngineConfigV2 = exports.runEngineV2 = exports.loadBabelizeConfig = exports.BabelizeEngine = void 0;
25
- exports.TRANSLATION_CACHE_TTL_SECONDS = exports.translationCacheKey = exports.hashText = exports.readProjectLocaleConfig = exports.localeIssueFile = exports.checkProjectLocales = exports.ReviewError = exports.reviewLocaleChanges = exports.loadLocale = exports.isLocaleCode = exports.isIgnoredLocalePath = exports.groupLocaleFiles = exports.findLocaleFileSets = exports.DEFAULT_LOCALE_IGNORE_DIRS = void 0;
24
+ exports.analyzeWorkspace = exports.analyzeProject = exports.localeExtension = exports.getCircuitBreaker = exports.CircuitState = exports.CircuitBreaker = exports.validateBatchQuality = exports.validateTranslationQuality = exports.translatorLoadBalancer = exports.warmupTranslator = exports.checkTranslatorHealth = exports.callTranslator = exports.requiredPluralCategories = exports.parseIcu = exports.isIcuMessage = exports.compareIcuMessages = exports.sameProtectedSpans = exports.restoreText = exports.protectText = exports.hasNothingToTranslate = exports.checkTokens = exports.validateRawTokens = exports.sanitizeTranslation = exports.restorePlaceholders = exports.protectPlaceholders = exports.hashString = exports.hasICUSyntax = exports.extractPlaceholders = exports.parseCode = exports.FRAMEWORK_DISPLAY_NAMES = exports.localRepoFiles = exports.detectStack = exports.saveProjectConfig = exports.saveCachedStrings = exports.getProjectInfo = exports.getBabelizeSummary = exports.detectProjectInfo = exports.addBabelToHistory = exports.formatSyncNotificationMessage = exports.detectSyncIssues = exports.matchFromMemory = exports.setEngineLogger = exports.runWithEngineLogger = exports.validateWorkspace = exports.scanKeyReferences = exports.RunAbortedError = exports.validateEngineConfigV2 = exports.runEngineV2 = exports.loadBabelizeConfig = exports.BabelizeEngine = void 0;
25
+ exports.TRANSLATION_CACHE_TTL_SECONDS = exports.translationCacheKey = exports.hashText = exports.readProjectLocaleConfig = exports.localeIssueFile = exports.checkProjectLocales = exports.ReviewError = exports.reviewLocaleChanges = exports.loadLocale = exports.isLocaleCode = exports.isIgnoredLocalePath = exports.groupLocaleFiles = exports.findLocaleFileSets = exports.DEFAULT_LOCALE_IGNORE_DIRS = exports.glossaryFingerprint = exports.normalizeGlossary = exports.localeValue = exports.containsTerm = exports.canonicalizeLocaleKeys = exports.canonicalLocale = exports.tallyLocaleIssues = exports.flattenLocaleMessages = exports.checkLocalePair = exports.frameworkSupport = void 0;
26
26
  var BabelizeEngine_1 = require("./BabelizeEngine");
27
27
  Object.defineProperty(exports, "BabelizeEngine", { enumerable: true, get: function () { return BabelizeEngine_1.BabelizeEngine; } });
28
28
  var config_1 = require("./config");
@@ -31,6 +31,8 @@ Object.defineProperty(exports, "loadBabelizeConfig", { enumerable: true, get: fu
31
31
  var runtime_1 = require("./runtime");
32
32
  Object.defineProperty(exports, "runEngineV2", { enumerable: true, get: function () { return runtime_1.runEngineV2; } });
33
33
  Object.defineProperty(exports, "validateEngineConfigV2", { enumerable: true, get: function () { return runtime_1.validateEngineConfigV2; } });
34
+ var orchestrator_1 = require("./orchestrator");
35
+ Object.defineProperty(exports, "RunAbortedError", { enumerable: true, get: function () { return orchestrator_1.RunAbortedError; } });
34
36
  var key_usage_1 = require("./utils/key-usage");
35
37
  Object.defineProperty(exports, "scanKeyReferences", { enumerable: true, get: function () { return key_usage_1.scanKeyReferences; } });
36
38
  // Framework detection without running the pipeline (CLI `translate` uses it
@@ -58,6 +60,10 @@ Object.defineProperty(exports, "getBabelizeSummary", { enumerable: true, get: fu
58
60
  Object.defineProperty(exports, "getProjectInfo", { enumerable: true, get: function () { return metadata_1.getProjectInfo; } });
59
61
  Object.defineProperty(exports, "saveCachedStrings", { enumerable: true, get: function () { return metadata_1.saveCachedStrings; } });
60
62
  Object.defineProperty(exports, "saveProjectConfig", { enumerable: true, get: function () { return metadata_1.saveProjectConfig; } });
63
+ var stack_1 = require("./detection/stack");
64
+ Object.defineProperty(exports, "detectStack", { enumerable: true, get: function () { return stack_1.detectStack; } });
65
+ Object.defineProperty(exports, "localRepoFiles", { enumerable: true, get: function () { return stack_1.localRepoFiles; } });
66
+ Object.defineProperty(exports, "FRAMEWORK_DISPLAY_NAMES", { enumerable: true, get: function () { return stack_1.FRAMEWORK_DISPLAY_NAMES; } });
61
67
  var ast_helpers_1 = require("./utils/ast-helpers");
62
68
  Object.defineProperty(exports, "parseCode", { enumerable: true, get: function () { return ast_helpers_1.parseCode; } });
63
69
  var string_utils_1 = require("./utils/string-utils");
@@ -104,6 +110,14 @@ var locale_qa_1 = require("./qa/locale-qa");
104
110
  Object.defineProperty(exports, "checkLocalePair", { enumerable: true, get: function () { return locale_qa_1.checkLocalePair; } });
105
111
  Object.defineProperty(exports, "flattenLocaleMessages", { enumerable: true, get: function () { return locale_qa_1.flattenLocaleMessages; } });
106
112
  Object.defineProperty(exports, "tallyLocaleIssues", { enumerable: true, get: function () { return locale_qa_1.tallyLocaleIssues; } });
113
+ // Glossary rules: term matching, locale lookup/canonicalization, conflicts.
114
+ var glossary_1 = require("./modules/glossary");
115
+ Object.defineProperty(exports, "canonicalLocale", { enumerable: true, get: function () { return glossary_1.canonicalLocale; } });
116
+ Object.defineProperty(exports, "canonicalizeLocaleKeys", { enumerable: true, get: function () { return glossary_1.canonicalizeLocaleKeys; } });
117
+ Object.defineProperty(exports, "containsTerm", { enumerable: true, get: function () { return glossary_1.containsTerm; } });
118
+ Object.defineProperty(exports, "localeValue", { enumerable: true, get: function () { return glossary_1.localeValue; } });
119
+ Object.defineProperty(exports, "normalizeGlossary", { enumerable: true, get: function () { return glossary_1.normalizeGlossary; } });
120
+ Object.defineProperty(exports, "glossaryFingerprint", { enumerable: true, get: function () { return glossary_1.glossaryFingerprint; } });
107
121
  var locale_files_1 = require("./qa/locale-files");
108
122
  Object.defineProperty(exports, "DEFAULT_LOCALE_IGNORE_DIRS", { enumerable: true, get: function () { return locale_files_1.DEFAULT_LOCALE_IGNORE_DIRS; } });
109
123
  Object.defineProperty(exports, "findLocaleFileSets", { enumerable: true, get: function () { return locale_files_1.findLocaleFileSets; } });
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PseudoTranslationProvider = exports.PSEUDO_OPEN = exports.PSEUDO_CLOSE = exports.isPseudoLocalized = void 0;
4
4
  const icu_message_1 = require("../utils/icu-message");
5
+ const rules_1 = require("../modules/glossary/rules");
5
6
  /**
6
7
  * Pseudo-localization: a deterministic, offline TranslationProvider for tests
7
8
  * and for checking i18n wiring without an API key (`babelize translate
@@ -27,6 +28,9 @@ const ACCENTS = {
27
28
  /** Spans that must survive untouched: engine tokens, args, tags, URLs, emails, code, identifiers. */
28
29
  const PROTECTED = new RegExp([
29
30
  String.raw `\[\s*#\s*VAR_\d+\s*#\s*\]`,
31
+ // printf / gettext placeholders and HTML entities: %(name)s, %d, %%, &mdash;
32
+ String.raw `%\(\w+\)[-#0 +]*\d*(?:\.\d+)?[sdifr]|%%|%[sdif]`,
33
+ String.raw `&(?:[a-zA-Z][a-zA-Z0-9]{1,31}|#\d{1,7}|#x[0-9a-fA-F]{1,6});`,
30
34
  String.raw `\{[^{}]*\}`,
31
35
  String.raw `<[^<>]+>`,
32
36
  String.raw `https?:\/\/\S+`,
@@ -40,9 +44,6 @@ const PROTECTED = new RegExp([
40
44
  function accent(text) {
41
45
  return text.replace(/[aeiouycnAEIOUYCN]/g, (ch) => ACCENTS[ch] ?? ch);
42
46
  }
43
- function escapeRegExp(s) {
44
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
45
- }
46
47
  /** Accent everything outside protected spans; glossary terms become their required translation. */
47
48
  function pseudoText(text, glossary = []) {
48
49
  const terms = [...glossary].sort((a, b) => b.term.length - a.term.length);
@@ -53,7 +54,7 @@ function pseudoText(text, glossary = []) {
53
54
  while (rest) {
54
55
  let best = null;
55
56
  for (const g of terms) {
56
- const m = new RegExp(`(?<![\\w])${escapeRegExp(g.term)}(?![\\w])`, 'i').exec(rest);
57
+ const m = new RegExp((0, rules_1.termPattern)(g.term), 'iu').exec(rest);
57
58
  if (m && (!best || m.index < best.index))
58
59
  best = { index: m.index, length: m[0].length, translation: g.translation };
59
60
  }
@@ -1,5 +1,8 @@
1
1
  import type { EngineConfigV2, EngineResultV2 } from '../runtime/types';
2
- import type { TranslationFailure, TranslationProvider } from '../providers/types';
2
+ import type { ProjectRulesStore, TranslationFailure, TranslationProvider } from '../providers/types';
3
+ import type { LocaleGlossary } from '../qa/locale-qa';
4
+ /** A rules store serving a `.babelize/config.json` glossary (no brand voice locally). */
5
+ export declare function configRulesStore(glossary: LocaleGlossary): ProjectRulesStore;
3
6
  import { type ByokProvider } from '@babelize/types';
4
7
  import type { HostedTranslationProvider } from './hosted-provider';
5
8
  import { type FormatResult } from './formatter';
@@ -7,7 +10,7 @@ import { type FileDiffStat, type ApplyIo, type WorkspaceChanges } from './worksp
7
10
  /** Lockfile scope for local runs: `.babelize/local/babelize-lock.json` in the project root. */
8
11
  export declare const LOCAL_PROJECT_ID = "local";
9
12
  export declare const LOCAL_LOCKFILE = ".babelize/local/babelize-lock.json";
10
- export declare const SUPPORTED_LOCAL_FRAMEWORKS: readonly ["react", "nextjs"];
13
+ export declare const SUPPORTED_LOCAL_FRAMEWORKS: readonly ["react", "nextjs", "django"];
11
14
  export declare const BETA_LOCAL_FRAMEWORKS: readonly ["vue", "static"];
12
15
  export interface LocalTranslateOptions {
13
16
  projectDir: string;
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.LocalTranslateError = exports.BETA_LOCAL_FRAMEWORKS = exports.SUPPORTED_LOCAL_FRAMEWORKS = exports.LOCAL_LOCKFILE = exports.LOCAL_PROJECT_ID = void 0;
37
+ exports.configRulesStore = configRulesStore;
37
38
  exports.parseLocales = parseLocales;
38
39
  exports.validateLocales = validateLocales;
39
40
  exports.sentence = sentence;
@@ -45,10 +46,23 @@ const promises_1 = require("node:fs/promises");
45
46
  const path = __importStar(require("node:path"));
46
47
  const node_util_1 = require("node:util");
47
48
  const language_codes_1 = require("../modules/translator/language-codes");
49
+ const locale_names_1 = require("../django/locale-names");
48
50
  const lockfile_1 = require("../modules/lockfile");
49
51
  const orchestrator_1 = require("../orchestrator");
50
52
  const logger_1 = require("../utils/logger");
51
53
  const workspace_1 = require("../modules/workspace");
54
+ const check_project_1 = require("../qa/check-project");
55
+ /** A rules store serving a `.babelize/config.json` glossary (no brand voice locally). */
56
+ function configRulesStore(glossary) {
57
+ const rules = [
58
+ ...(glossary.neverTranslate ?? []).map((term) => ({ term, type: 'never_translate', translations: null })),
59
+ ...(glossary.alwaysTranslateAs ?? []).map((e) => ({ term: e.term, type: 'always_translate_as', translations: e.translations })),
60
+ ];
61
+ return {
62
+ getGlossary: async () => rules,
63
+ getBrandVoice: async () => null,
64
+ };
65
+ }
52
66
  const types_1 = require("@babelize/types");
53
67
  const git_1 = require("./git");
54
68
  const formatter_1 = require("./formatter");
@@ -58,7 +72,7 @@ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
58
72
  /** Lockfile scope for local runs: `.babelize/local/babelize-lock.json` in the project root. */
59
73
  exports.LOCAL_PROJECT_ID = 'local';
60
74
  exports.LOCAL_LOCKFILE = `.babelize/${exports.LOCAL_PROJECT_ID}/babelize-lock.json`;
61
- exports.SUPPORTED_LOCAL_FRAMEWORKS = ['react', 'nextjs'];
75
+ exports.SUPPORTED_LOCAL_FRAMEWORKS = ['react', 'nextjs', 'django'];
62
76
  exports.BETA_LOCAL_FRAMEWORKS = ['vue', 'static'];
63
77
  class LocalTranslateError extends Error {
64
78
  constructor(message, code,
@@ -96,7 +110,15 @@ async function listProjectFiles(dir) {
96
110
  /** Locales this project was already translated into: locale files plus lockfile entries. */
97
111
  async function existingTargetLocales(workspaceDir, publicDir, localeFormat, sourceLocale) {
98
112
  const found = new Set();
99
- if (localeFormat !== 'po') {
113
+ if (localeFormat === 'po') {
114
+ // Django: <locale dir>/<pt_BR>/LC_MESSAGES/django.po
115
+ for (const entry of await (0, promises_1.readdir)(path.join(workspaceDir, publicDir)).catch(() => [])) {
116
+ const po = path.join(workspaceDir, publicDir, entry, 'LC_MESSAGES', 'django.po');
117
+ if (await (0, promises_1.readFile)(po, 'utf8').then(() => true, () => false))
118
+ found.add((0, locale_names_1.localeFromDjangoName)(entry));
119
+ }
120
+ }
121
+ else {
100
122
  const ext = localeFormat === 'arb' ? '.arb' : '.json';
101
123
  for (const entry of await (0, promises_1.readdir)(path.join(workspaceDir, publicDir, 'locales')).catch(() => [])) {
102
124
  if (entry.endsWith(ext))
@@ -286,7 +308,7 @@ async function runScoped(opts, deps, engineLog, state) {
286
308
  }
287
309
  const frameworkStatus = frameworkStatusOf(framework);
288
310
  if (!frameworkStatus) {
289
- throw new LocalTranslateError(`Detected framework "${framework}", which \`babelize translate\` does not support. Supported: React and Next.js (Vue and static HTML in beta).`, 'unsupported_framework');
311
+ throw new LocalTranslateError(`Detected framework "${framework}", which \`babelize translate\` does not support. Supported: React, Next.js and Django (Vue and static HTML in beta).`, 'unsupported_framework');
290
312
  }
291
313
  deps.onFramework?.(framework, frameworkStatus);
292
314
  // The engine deletes locale files that aren't in its target list, so
@@ -299,6 +321,10 @@ async function runScoped(opts, deps, engineLog, state) {
299
321
  const targetLocales = [...opts.targetLocales, ...keptLocales];
300
322
  const before = await (0, workspace_copy_1.snapshot)(temp.dir);
301
323
  state.collecting = true;
324
+ // The project's glossary from .babelize/config.json — the same file and
325
+ // schema `babelize check` reads — applied like a cloud project's.
326
+ const { glossary } = await (0, check_project_1.readProjectLocaleConfig)(projectDir);
327
+ const rules = glossary ? configRulesStore(glossary) : undefined;
302
328
  const runEngine = deps.runEngine ?? orchestrator_1.runEngineV2;
303
329
  const result = await runEngine({
304
330
  workspacePath: temp.dir,
@@ -314,7 +340,7 @@ async function runScoped(opts, deps, engineLog, state) {
314
340
  // and the updated one comes back with the other changes — only on success.
315
341
  dataDir: temp.dir,
316
342
  byok: translator ? undefined : opts.byok,
317
- providers: translator ? { translator } : undefined,
343
+ providers: translator || rules ? { ...(translator ? { translator } : {}), ...(rules ? { rules } : {}) } : undefined,
318
344
  installDependencies: false,
319
345
  onProgress: (message, step, status) => deps.onProgress?.(message, step, status),
320
346
  });
@@ -348,9 +374,21 @@ async function runScoped(opts, deps, engineLog, state) {
348
374
  // cloud backend stages locale files for a PR.
349
375
  const lost = [];
350
376
  for (const locale of result.locales) {
351
- if (locale.locale === opts.sourceLocale)
377
+ if (locale.locale === opts.sourceLocale || locale.noFile)
352
378
  continue;
353
379
  const rel = locale.path.split(path.sep).join('/');
380
+ if (locale.content !== undefined) {
381
+ // A merged catalogue (Django .po): written exactly as the engine
382
+ // produced it. The merge never drops existing translations.
383
+ for (const f of [{ path: rel, content: locale.content, encoding: 'utf8' }, ...(locale.extraFiles ?? [])]) {
384
+ const abs = path.resolve(temp.dir, f.path.split(path.sep).join('/'));
385
+ if (path.relative(temp.dir, abs).startsWith('..'))
386
+ continue;
387
+ await (0, promises_1.mkdir)(path.dirname(abs), { recursive: true });
388
+ await (0, promises_1.writeFile)(abs, f.encoding === 'base64' ? Buffer.from(f.content, 'base64') : f.content);
389
+ }
390
+ continue;
391
+ }
354
392
  const previous = await readJsonIfExists(path.join(projectDir, rel));
355
393
  const next = flattenData(locale.data);
356
394
  const failedHere = new Set(translationFailures.filter((f) => f.locale === locale.locale).map((f) => f.key));
@@ -39,6 +39,7 @@ const node_os_1 = require("node:os");
39
39
  const path = __importStar(require("node:path"));
40
40
  const keygen_1 = require("../modules/keygen");
41
41
  const translate_1 = require("../modules/translator/translate");
42
+ const glossary_1 = require("../modules/glossary");
42
43
  const logger_1 = require("../utils/logger");
43
44
  async function translateMessages(opts) {
44
45
  const logger = {
@@ -54,14 +55,11 @@ async function run(opts) {
54
55
  throw new Error('translateMessages needs a byok key or a translator.');
55
56
  const dir = await (0, promises_1.mkdtemp)(path.join((0, node_os_1.tmpdir)(), 'babelize-messages-'));
56
57
  try {
57
- // The translate phase reads glossary rules from .babelize/config.json when there is no project id.
58
- const customTranslations = {};
59
- for (const term of opts.glossary?.neverTranslate ?? [])
60
- customTranslations[term] = { neverTranslate: true };
61
- for (const e of opts.glossary?.alwaysTranslateAs ?? [])
62
- customTranslations[e.term] = { translations: e.translations };
63
- await (0, promises_1.mkdir)(path.join(dir, '.babelize'), { recursive: true });
64
- await (0, promises_1.writeFile)(path.join(dir, '.babelize', 'config.json'), JSON.stringify({ customTranslations }));
58
+ // The glossary goes in as already-loaded rules, normalized like a project's.
59
+ const glossary = (0, glossary_1.normalizeGlossary)([
60
+ ...(opts.glossary?.neverTranslate ?? []).map((term) => ({ term, type: 'never_translate' })),
61
+ ...(opts.glossary?.alwaysTranslateAs ?? []).map((e) => ({ term: e.term, type: 'always_translate_as', translations: e.translations })),
62
+ ]);
65
63
  const workspace = {
66
64
  path: dir,
67
65
  framework: 'react',
@@ -72,7 +70,7 @@ async function run(opts) {
72
70
  localeFormat: 'json',
73
71
  };
74
72
  const result = await (0, translate_1.translateLocales)(workspace, { locale: opts.sourceLocale, path: '', data: { ...opts.messages }, isNew: false }, opts.targetLocales, false, true, // dry run: nothing written
75
- undefined, opts.onProgress, undefined, undefined, opts.translator ? undefined : opts.byok, undefined, undefined, opts.translator ? { translator: opts.translator } : {}, { targetPlurals: opts.targetPlurals ?? true });
73
+ undefined, opts.onProgress, undefined, undefined, opts.translator ? undefined : opts.byok, undefined, undefined, opts.translator ? { translator: opts.translator } : {}, { targetPlurals: opts.targetPlurals ?? true, rules: { glossary, brandVoice: null, complete: true } });
76
74
  const targets = {};
77
75
  for (const file of result.translation.locales) {
78
76
  const flat = (0, keygen_1.flattenLocaleData)(file.data);
@@ -42,8 +42,8 @@ const ast_helpers_1 = require("../../utils/ast-helpers");
42
42
  const file_utils_1 = require("../../utils/file-utils");
43
43
  const string_utils_1 = require("../../utils/string-utils");
44
44
  const detect_format_1 = require("../../utils/detect-format");
45
- const dtl_mask_1 = require("../../utils/dtl-mask");
46
- const py_extractor_1 = require("../../utils/py-extractor");
45
+ const extract_1 = require("../../django/extract");
46
+ const types_2 = require("@babelize/types");
47
47
  const jsx_message_1 = require("../../utils/jsx-message");
48
48
  const ui_copy_1 = require("../../utils/ui-copy");
49
49
  const module_constants_1 = require("../../utils/module-constants");
@@ -64,10 +64,14 @@ async function extractStrings(workspace, filesToProcess, extractOptions = {}) {
64
64
  }
65
65
  }
66
66
  else {
67
- // Otherwise scan all source directories
67
+ // Otherwise scan all source directories. Django scans .py/.html and
68
+ // skips virtualenvs, migrations, static files and the like.
69
+ const fwConfig = workspace.framework === 'django' ? types_2.FRAMEWORK_CONFIGS.django : undefined;
70
+ const extensions = fwConfig ? [...fwConfig.extensions] : types_1.SUPPORTED_EXTENSIONS;
71
+ const excluded = fwConfig ? [...types_1.EXCLUDED_PATTERNS, ...(fwConfig.excludedPatterns ?? [])] : types_1.EXCLUDED_PATTERNS;
68
72
  for (const sourceDir of workspace.sourceDirs) {
69
73
  const dirPath = path.join(workspace.path, sourceDir);
70
- const files = await (0, file_utils_1.findFiles)(dirPath, types_1.SUPPORTED_EXTENSIONS, types_1.EXCLUDED_PATTERNS);
74
+ const files = await (0, file_utils_1.findFiles)(dirPath, extensions, excluded);
71
75
  sourceFiles.push(...files);
72
76
  }
73
77
  }
@@ -104,17 +108,21 @@ async function extractStrings(workspace, filesToProcess, extractOptions = {}) {
104
108
  const extraction = { strings, filesAnalyzed, skippedFiles, warnings, notInjected };
105
109
  return { extraction };
106
110
  }
111
+ const GENERATED_FILE_MARKERS = ['@babelize-generated', 'babelize:i18n-config'];
112
+ /** True for files Babelize wrote itself; the marker sits in the header comment. */
113
+ function isBabelizeGenerated(content) {
114
+ const head = content.slice(0, 500);
115
+ return GENERATED_FILE_MARKERS.some((marker) => head.includes(marker));
116
+ }
107
117
  async function extractStringsFromFile(content, filePath, detectFormats, options) {
108
- // Django template handling
109
- if (options?.framework === 'django' && (filePath.endsWith('.html') || filePath.endsWith('.htm'))) {
110
- const { masked } = (0, dtl_mask_1.maskTemplateTags)(content);
111
- const result = await (0, html_extractor_1.extractStringsFromHtml)(masked, filePath, detectFormats);
112
- // Don't restore — we extract from the masked version, the restore happens at inject time
113
- return result;
114
- }
115
- // Python file handling
116
- if (filePath.endsWith('.py')) {
117
- return (0, py_extractor_1.extractStringsFromPython)(content, filePath);
118
+ // Files Babelize generated itself (i18n config, next-intl request config)
119
+ // hold config values such as bindI18n: 'languageChanged loaded'. A re-run
120
+ // used to extract those and inject t() into the i18n config, breaking it.
121
+ if (isBabelizeGenerated(content))
122
+ return { strings: [], warnings: [] };
123
+ // Django: gettext calls, translate tags and hardcoded text in templates/forms/models/views.
124
+ if (options?.framework === 'django') {
125
+ return (0, extract_1.extractDjangoFile)(content, filePath, { icu: options.plurals === true });
118
126
  }
119
127
  if (filePath.endsWith('.html') || filePath.endsWith('.htm')) {
120
128
  return (0, html_extractor_1.extractStringsFromHtml)(content, filePath, detectFormats);
@@ -4,10 +4,25 @@ export interface DjangoFinalizeResult {
4
4
  /** Human-readable description of each edit applied. */
5
5
  changes: string[];
6
6
  warnings: string[];
7
+ /** Settings file content before and after, when it changed. */
8
+ original?: string;
9
+ updated?: string;
7
10
  }
8
11
  export declare function resolveSettingsPath(workspacePath: string): Promise<string | null>;
12
+ /**
13
+ * The locale directory Django reads (LOCALE_PATHS[0]), relative to the
14
+ * workspace, '/'-separated. Falls back to <BASE_DIR>/locale — what finalize
15
+ * adds — when LOCALE_PATHS is absent or not a plain BASE_DIR path.
16
+ */
17
+ export declare function resolveDjangoLocaleDir(workspacePath: string, djangoRoot: string): Promise<string>;
18
+ /** First LOCALE_PATHS entry as a path relative to BASE_DIR, if it is one. */
19
+ export declare function parseLocalePaths(source: string): string | null;
9
20
  export declare function finalizeDjangoSettings(workspacePath: string, options?: {
10
21
  targetLocales?: string[];
11
22
  sourceLocale?: string;
12
23
  dryRun?: boolean;
24
+ /** Django project root (BASE_DIR), relative to the workspace. */
25
+ djangoRoot?: string;
26
+ /** Locale directory the catalogues were written to, relative to the workspace. */
27
+ localeDir?: string;
13
28
  }): Promise<DjangoFinalizeResult>;