@babelize/engine 0.0.0-stage → 0.1.2

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 (270) hide show
  1. package/LICENSE +202 -0
  2. package/dist/BabelizeEngine.d.ts +12 -0
  3. package/dist/BabelizeEngine.js +47 -0
  4. package/dist/analysis/analyze.d.ts +106 -0
  5. package/dist/analysis/analyze.js +323 -0
  6. package/dist/config/env.d.ts +9 -0
  7. package/dist/config/env.js +16 -0
  8. package/dist/config.d.ts +74 -0
  9. package/dist/config.js +47 -0
  10. package/dist/detection/stack.d.ts +42 -0
  11. package/dist/detection/stack.js +277 -0
  12. package/dist/django/catalog.d.ts +92 -0
  13. package/dist/django/catalog.js +677 -0
  14. package/dist/django/extract.d.ts +20 -0
  15. package/dist/django/extract.js +79 -0
  16. package/dist/django/inject-file.d.ts +6 -0
  17. package/dist/django/inject-file.js +52 -0
  18. package/dist/django/inject.d.ts +11 -0
  19. package/dist/django/inject.js +205 -0
  20. package/dist/django/keys.d.ts +21 -0
  21. package/dist/django/keys.js +63 -0
  22. package/dist/django/locale-names.d.ts +13 -0
  23. package/dist/django/locale-names.js +60 -0
  24. package/dist/django/mo-file.d.ts +6 -0
  25. package/dist/django/mo-file.js +60 -0
  26. package/dist/django/plural-forms.d.ts +34 -0
  27. package/dist/django/plural-forms.js +216 -0
  28. package/dist/django/po-file.d.ts +38 -0
  29. package/dist/django/po-file.js +273 -0
  30. package/dist/django/python-extract.d.ts +34 -0
  31. package/dist/django/python-extract.js +619 -0
  32. package/dist/django/python-lexer.d.ts +65 -0
  33. package/dist/django/python-lexer.js +398 -0
  34. package/dist/django/template-extract.d.ts +32 -0
  35. package/dist/django/template-extract.js +680 -0
  36. package/dist/django/types.d.ts +49 -0
  37. package/dist/django/types.js +15 -0
  38. package/dist/index.d.ts +59 -0
  39. package/dist/index.js +140 -0
  40. package/dist/injection/InjectionStrategy.d.ts +62 -0
  41. package/dist/injection/InjectionStrategy.js +2 -0
  42. package/dist/injection/StrategyFactory.d.ts +4 -0
  43. package/dist/injection/StrategyFactory.js +29 -0
  44. package/dist/injection/html/html-injector.d.ts +11 -0
  45. package/dist/injection/html/html-injector.js +173 -0
  46. package/dist/injection/strategies/NextIntlStrategy.d.ts +25 -0
  47. package/dist/injection/strategies/NextIntlStrategy.js +141 -0
  48. package/dist/injection/strategies/ReactI18nextStrategy.d.ts +19 -0
  49. package/dist/injection/strategies/ReactI18nextStrategy.js +116 -0
  50. package/dist/injection/strategies/VanillaStrategy.d.ts +11 -0
  51. package/dist/injection/strategies/VanillaStrategy.js +73 -0
  52. package/dist/injection/strategies/VueI18nStrategy.d.ts +11 -0
  53. package/dist/injection/strategies/VueI18nStrategy.js +84 -0
  54. package/dist/local/formatter.d.ts +26 -0
  55. package/dist/local/formatter.js +105 -0
  56. package/dist/local/git.d.ts +23 -0
  57. package/dist/local/git.js +136 -0
  58. package/dist/local/hosted-provider.d.ts +68 -0
  59. package/dist/local/hosted-provider.js +215 -0
  60. package/dist/local/index.d.ts +23 -0
  61. package/dist/local/index.js +75 -0
  62. package/dist/local/provider.d.ts +56 -0
  63. package/dist/local/provider.js +119 -0
  64. package/dist/local/pseudo-provider.d.ts +18 -0
  65. package/dist/local/pseudo-provider.js +208 -0
  66. package/dist/local/report.d.ts +177 -0
  67. package/dist/local/report.js +186 -0
  68. package/dist/local/translate-core.d.ts +218 -0
  69. package/dist/local/translate-core.js +499 -0
  70. package/dist/local/translate-messages.d.ts +49 -0
  71. package/dist/local/translate-messages.js +89 -0
  72. package/dist/local/workspace-copy.d.ts +91 -0
  73. package/dist/local/workspace-copy.js +355 -0
  74. package/dist/modules/extractor/extract.d.ts +20 -0
  75. package/dist/modules/extractor/extract.js +709 -0
  76. package/dist/modules/extractor/index.d.ts +2 -0
  77. package/dist/modules/extractor/index.js +63 -0
  78. package/dist/modules/finalization/django-finalize.d.ts +28 -0
  79. package/dist/modules/finalization/django-finalize.js +412 -0
  80. package/dist/modules/finalization/finalize.d.ts +38 -0
  81. package/dist/modules/finalization/finalize.js +745 -0
  82. package/dist/modules/finalization/index.d.ts +1 -0
  83. package/dist/modules/finalization/index.js +5 -0
  84. package/dist/modules/finalization/nextjs-finalize.d.ts +40 -0
  85. package/dist/modules/finalization/nextjs-finalize.js +328 -0
  86. package/dist/modules/finalization/static-runtime.d.ts +9 -0
  87. package/dist/modules/finalization/static-runtime.js +261 -0
  88. package/dist/modules/finalization/writes.d.ts +29 -0
  89. package/dist/modules/finalization/writes.js +78 -0
  90. package/dist/modules/format-injection/index.d.ts +19 -0
  91. package/dist/modules/format-injection/index.js +273 -0
  92. package/dist/modules/format-injection/inject-formats.d.ts +31 -0
  93. package/dist/modules/format-injection/inject-formats.js +208 -0
  94. package/dist/modules/glossary/index.d.ts +2 -0
  95. package/dist/modules/glossary/index.js +16 -0
  96. package/dist/modules/glossary/rules.d.ts +81 -0
  97. package/dist/modules/glossary/rules.js +244 -0
  98. package/dist/modules/glossary/types.d.ts +22 -0
  99. package/dist/modules/glossary/types.js +2 -0
  100. package/dist/modules/injection/index.d.ts +1 -0
  101. package/dist/modules/injection/index.js +6 -0
  102. package/dist/modules/injection/inject.d.ts +29 -0
  103. package/dist/modules/injection/inject.js +1050 -0
  104. package/dist/modules/keygen/index.d.ts +1 -0
  105. package/dist/modules/keygen/index.js +11 -0
  106. package/dist/modules/keygen/keygen.d.ts +4 -0
  107. package/dist/modules/keygen/keygen.js +123 -0
  108. package/dist/modules/keygen/locale-data.d.ts +7 -0
  109. package/dist/modules/keygen/locale-data.js +73 -0
  110. package/dist/modules/keygen/public.d.ts +2 -0
  111. package/dist/modules/keygen/public.js +12 -0
  112. package/dist/modules/locale-builder/index.d.ts +1 -0
  113. package/dist/modules/locale-builder/index.js +7 -0
  114. package/dist/modules/locale-builder/locales.d.ts +13 -0
  115. package/dist/modules/locale-builder/locales.js +130 -0
  116. package/dist/modules/lockfile/hasher.d.ts +7 -0
  117. package/dist/modules/lockfile/hasher.js +13 -0
  118. package/dist/modules/lockfile/index.d.ts +3 -0
  119. package/dist/modules/lockfile/index.js +22 -0
  120. package/dist/modules/lockfile/lockfile-manager.d.ts +99 -0
  121. package/dist/modules/lockfile/lockfile-manager.js +497 -0
  122. package/dist/modules/lockfile/schema.d.ts +196 -0
  123. package/dist/modules/lockfile/schema.js +56 -0
  124. package/dist/modules/parser/index.d.ts +2 -0
  125. package/dist/modules/parser/index.js +9 -0
  126. package/dist/modules/summary/index.d.ts +1 -0
  127. package/dist/modules/summary/index.js +7 -0
  128. package/dist/modules/summary/summary.d.ts +9 -0
  129. package/dist/modules/summary/summary.js +195 -0
  130. package/dist/modules/sync-detector/index.d.ts +2 -0
  131. package/dist/modules/sync-detector/index.js +7 -0
  132. package/dist/modules/sync-detector/reporter.d.ts +3 -0
  133. package/dist/modules/sync-detector/reporter.js +16 -0
  134. package/dist/modules/sync-detector/sync-detector.d.ts +39 -0
  135. package/dist/modules/sync-detector/sync-detector.js +374 -0
  136. package/dist/modules/translator/batch-optimizer.d.ts +12 -0
  137. package/dist/modules/translator/batch-optimizer.js +30 -0
  138. package/dist/modules/translator/client/byok.provider.d.ts +64 -0
  139. package/dist/modules/translator/client/byok.provider.js +319 -0
  140. package/dist/modules/translator/client/direct-translator.client.d.ts +12 -0
  141. package/dist/modules/translator/client/direct-translator.client.js +71 -0
  142. package/dist/modules/translator/client/gemini.provider.d.ts +1 -0
  143. package/dist/modules/translator/client/gemini.provider.js +110 -0
  144. package/dist/modules/translator/client/groq.provider.d.ts +1 -0
  145. package/dist/modules/translator/client/groq.provider.js +111 -0
  146. package/dist/modules/translator/client/request.types.d.ts +1 -0
  147. package/dist/modules/translator/client/request.types.js +2 -0
  148. package/dist/modules/translator/client/retry.d.ts +29 -0
  149. package/dist/modules/translator/client/retry.js +94 -0
  150. package/dist/modules/translator/client/test-utils/mock.client.d.ts +31 -0
  151. package/dist/modules/translator/client/test-utils/mock.client.js +59 -0
  152. package/dist/modules/translator/client/translator.interface.d.ts +1 -0
  153. package/dist/modules/translator/client/translator.interface.js +2 -0
  154. package/dist/modules/translator/icu-branches.d.ts +138 -0
  155. package/dist/modules/translator/icu-branches.js +393 -0
  156. package/dist/modules/translator/language-codes/errors.d.ts +10 -0
  157. package/dist/modules/translator/language-codes/errors.js +18 -0
  158. package/dist/modules/translator/language-codes/flores-map.d.ts +1 -0
  159. package/dist/modules/translator/language-codes/flores-map.js +107 -0
  160. package/dist/modules/translator/language-codes/index.d.ts +11 -0
  161. package/dist/modules/translator/language-codes/index.js +24 -0
  162. package/dist/modules/translator/language-codes/language-mapper.d.ts +25 -0
  163. package/dist/modules/translator/language-codes/language-mapper.js +132 -0
  164. package/dist/modules/translator/language-codes/rtl-languages.d.ts +2 -0
  165. package/dist/modules/translator/language-codes/rtl-languages.js +9 -0
  166. package/dist/modules/translator/memory/memory-matcher.d.ts +8 -0
  167. package/dist/modules/translator/memory/memory-matcher.js +218 -0
  168. package/dist/modules/translator/memory/types.d.ts +22 -0
  169. package/dist/modules/translator/memory/types.js +2 -0
  170. package/dist/modules/translator/translate.d.ts +74 -0
  171. package/dist/modules/translator/translate.js +1047 -0
  172. package/dist/modules/vue/migrate.d.ts +13 -0
  173. package/dist/modules/vue/migrate.js +40 -0
  174. package/dist/modules/vue/script-inject.d.ts +16 -0
  175. package/dist/modules/vue/script-inject.js +115 -0
  176. package/dist/modules/vue/sfc.d.ts +11 -0
  177. package/dist/modules/vue/sfc.js +51 -0
  178. package/dist/modules/vue/template-rewrite.d.ts +25 -0
  179. package/dist/modules/vue/template-rewrite.js +76 -0
  180. package/dist/modules/vue/template-scan.d.ts +29 -0
  181. package/dist/modules/vue/template-scan.js +264 -0
  182. package/dist/modules/workspace/index.d.ts +1 -0
  183. package/dist/modules/workspace/index.js +5 -0
  184. package/dist/modules/workspace/validate.d.ts +13 -0
  185. package/dist/modules/workspace/validate.js +333 -0
  186. package/dist/orchestrator.d.ts +13 -0
  187. package/dist/orchestrator.js +727 -0
  188. package/dist/providers/default-translation-provider.d.ts +20 -0
  189. package/dist/providers/default-translation-provider.js +30 -0
  190. package/dist/providers/index.d.ts +2 -0
  191. package/dist/providers/index.js +21 -0
  192. package/dist/providers/types.d.ts +153 -0
  193. package/dist/providers/types.js +2 -0
  194. package/dist/qa/check-project.d.ts +50 -0
  195. package/dist/qa/check-project.js +193 -0
  196. package/dist/qa/gettext-files.d.ts +15 -0
  197. package/dist/qa/gettext-files.js +115 -0
  198. package/dist/qa/locale-files.d.ts +44 -0
  199. package/dist/qa/locale-files.js +159 -0
  200. package/dist/qa/locale-qa.d.ts +61 -0
  201. package/dist/qa/locale-qa.js +342 -0
  202. package/dist/qa/review.d.ts +43 -0
  203. package/dist/qa/review.js +175 -0
  204. package/dist/runtime/config.d.ts +8 -0
  205. package/dist/runtime/config.js +37 -0
  206. package/dist/runtime/index.d.ts +3 -0
  207. package/dist/runtime/index.js +8 -0
  208. package/dist/runtime/types.d.ts +427 -0
  209. package/dist/runtime/types.js +38 -0
  210. package/dist/types.d.ts +134 -0
  211. package/dist/types.js +12 -0
  212. package/dist/utils/ast-helpers.d.ts +57 -0
  213. package/dist/utils/ast-helpers.js +608 -0
  214. package/dist/utils/cache-keys.d.ts +8 -0
  215. package/dist/utils/cache-keys.js +18 -0
  216. package/dist/utils/circuit-breaker.d.ts +43 -0
  217. package/dist/utils/circuit-breaker.js +119 -0
  218. package/dist/utils/concurrent-map.d.ts +4 -0
  219. package/dist/utils/concurrent-map.js +21 -0
  220. package/dist/utils/detect-format.d.ts +5 -0
  221. package/dist/utils/detect-format.js +125 -0
  222. package/dist/utils/extraction-options.d.ts +29 -0
  223. package/dist/utils/extraction-options.js +33 -0
  224. package/dist/utils/file-utils.d.ts +33 -0
  225. package/dist/utils/file-utils.js +267 -0
  226. package/dist/utils/glossary-match.d.ts +9 -0
  227. package/dist/utils/glossary-match.js +25 -0
  228. package/dist/utils/html-extractor.d.ts +8 -0
  229. package/dist/utils/html-extractor.js +317 -0
  230. package/dist/utils/i18next-icu.d.ts +38 -0
  231. package/dist/utils/i18next-icu.js +159 -0
  232. package/dist/utils/icu-message.d.ts +65 -0
  233. package/dist/utils/icu-message.js +346 -0
  234. package/dist/utils/icu-spans.d.ts +10 -0
  235. package/dist/utils/icu-spans.js +126 -0
  236. package/dist/utils/jsx-message.d.ts +93 -0
  237. package/dist/utils/jsx-message.js +628 -0
  238. package/dist/utils/key-usage.d.ts +35 -0
  239. package/dist/utils/key-usage.js +247 -0
  240. package/dist/utils/logger.d.ts +24 -0
  241. package/dist/utils/logger.js +33 -0
  242. package/dist/utils/metadata.d.ts +82 -0
  243. package/dist/utils/metadata.js +261 -0
  244. package/dist/utils/module-constants.d.ts +30 -0
  245. package/dist/utils/module-constants.js +265 -0
  246. package/dist/utils/not-injected.d.ts +6 -0
  247. package/dist/utils/not-injected.js +38 -0
  248. package/dist/utils/placeholder-protection.d.ts +74 -0
  249. package/dist/utils/placeholder-protection.js +181 -0
  250. package/dist/utils/pseudo-markers.d.ts +9 -0
  251. package/dist/utils/pseudo-markers.js +16 -0
  252. package/dist/utils/string-utils.d.ts +52 -0
  253. package/dist/utils/string-utils.js +270 -0
  254. package/dist/utils/translation-quality.d.ts +34 -0
  255. package/dist/utils/translation-quality.js +156 -0
  256. package/dist/utils/translation-sanitizer.d.ts +11 -0
  257. package/dist/utils/translation-sanitizer.js +71 -0
  258. package/dist/utils/translator-caller.d.ts +32 -0
  259. package/dist/utils/translator-caller.js +217 -0
  260. package/dist/utils/translator-load-balancer.d.ts +73 -0
  261. package/dist/utils/translator-load-balancer.js +297 -0
  262. package/dist/utils/ui-copy.d.ts +32 -0
  263. package/dist/utils/ui-copy.js +126 -0
  264. package/dist/utils/untranslated.d.ts +15 -0
  265. package/dist/utils/untranslated.js +34 -0
  266. package/dist/utils/workspace.d.ts +14 -0
  267. package/dist/utils/workspace.js +89 -0
  268. package/package.json +64 -4
  269. package/schemas/translate-report.schema.json +297 -0
  270. package/README.md +0 -3
@@ -0,0 +1,619 @@
1
+ "use strict";
2
+ /**
3
+ * Django Python extraction.
4
+ *
5
+ * Finds two kinds of strings in a .py file:
6
+ *
7
+ * 1. Strings already marked for translation — `_()`, `gettext()`,
8
+ * `gettext_lazy()`, `ngettext()`, `pgettext()`, `npgettext()` and their
9
+ * lazy/legacy variants, under whatever local names the file imports them
10
+ * as. These go into the catalogue as-is.
11
+ *
12
+ * 2. Hardcoded user-facing strings in places Django renders to users, which
13
+ * the injector wraps with gettext / gettext_lazy:
14
+ * - verbose_name, verbose_name_plural, help_text, label, empty_label,
15
+ * empty_value_display keyword arguments; `description=` on
16
+ * @admin.action / @admin.display; `message=` on validators;
17
+ * - labels in `choices` (CHOICES lists, choices=, TextChoices members);
18
+ * - error_messages dicts and widget attrs (placeholder, title, …);
19
+ * - messages.info/success/… and ModelAdmin.message_user() text,
20
+ * ValidationError messages — and string variables passed to them;
21
+ * - text returned by template tags and filters (templatetags/*.py).
22
+ *
23
+ * Everything else (dict keys, identifiers, log lines, URLs, codes) is left
24
+ * alone: wrapping a string that is not shown to users changes behaviour.
25
+ * f-strings become `_("… %(name)s …") % {"name": expr}`; f-strings this can't
26
+ * rewrite safely are reported, not guessed at.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.looksLikeNaturalLanguage = looksLikeNaturalLanguage;
30
+ exports.extractDjangoPython = extractDjangoPython;
31
+ const python_lexer_1 = require("./python-lexer");
32
+ const types_1 = require("./types");
33
+ const CANONICAL = {
34
+ gettext: 'gettext', ugettext: 'gettext',
35
+ gettext_lazy: 'gettext_lazy', ugettext_lazy: 'gettext_lazy',
36
+ gettext_noop: 'gettext_noop', ugettext_noop: 'gettext_noop',
37
+ ngettext: 'ngettext', ungettext: 'ngettext',
38
+ ngettext_lazy: 'ngettext_lazy', ungettext_lazy: 'ngettext_lazy',
39
+ pgettext: 'pgettext', pgettext_lazy: 'pgettext_lazy',
40
+ npgettext: 'npgettext', npgettext_lazy: 'npgettext_lazy',
41
+ };
42
+ /** Argument positions (and keyword names) of each gettext function. */
43
+ const SIGNATURES = {
44
+ gettext: { singular: 0 }, gettext_lazy: { singular: 0 }, gettext_noop: { singular: 0 },
45
+ ngettext: { singular: 0, plural: 1 }, ngettext_lazy: { singular: 0, plural: 1 },
46
+ pgettext: { ctx: 0, singular: 1 }, pgettext_lazy: { ctx: 0, singular: 1 },
47
+ npgettext: { ctx: 0, singular: 1, plural: 2 }, npgettext_lazy: { ctx: 0, singular: 1, plural: 2 },
48
+ };
49
+ const KEYWORDS = new Set([
50
+ 'if', 'elif', 'else', 'while', 'for', 'in', 'not', 'and', 'or', 'is', 'return', 'yield', 'lambda', 'assert',
51
+ 'del', 'with', 'as', 'raise', 'from', 'import', 'await', 'print', 'case', 'match', 'except', 'global', 'nonlocal',
52
+ ]);
53
+ /** Keyword arguments whose string value Django shows to users. */
54
+ const UI_KWARGS = new Set(['verbose_name', 'verbose_name_plural', 'help_text', 'label', 'empty_label', 'empty_value_display']);
55
+ /** Keys of widget `attrs` dicts that hold visible text. */
56
+ const UI_ATTR_KEYS = new Set(['placeholder', 'title', 'aria-label', 'alt', 'aria-description']);
57
+ const MESSAGE_FUNCS = /^messages\.(?:debug|info|success|warning|error)$/;
58
+ /** Local names for django.utils.translation functions, from the file's imports. */
59
+ function importAliases(tokens) {
60
+ const names = new Map();
61
+ const modules = new Set(['django.utils.translation']);
62
+ for (let i = 0; i < tokens.length; i++) {
63
+ const t = tokens[i];
64
+ if (t.type !== 'name')
65
+ continue;
66
+ if (t.value === 'from') {
67
+ // from django.utils.translation import a as b, c
68
+ let j = i + 1;
69
+ let mod = '';
70
+ while (j < tokens.length && (tokens[j].type === 'name' || tokens[j].value === '.') && tokens[j].value !== 'import')
71
+ mod += tokens[j++].value;
72
+ if (tokens[j]?.value !== 'import')
73
+ continue;
74
+ j++;
75
+ const paren = tokens[j]?.value === '(';
76
+ if (paren)
77
+ j++;
78
+ const line = tokens[j]?.line;
79
+ while (j < tokens.length) {
80
+ const n = tokens[j];
81
+ if (n.type !== 'name') {
82
+ if (n.value === ',') {
83
+ j++;
84
+ continue;
85
+ }
86
+ break;
87
+ }
88
+ if (!paren && n.line !== line && tokens[j - 1]?.value !== ',')
89
+ break;
90
+ let local = n.value;
91
+ const imported = n.value;
92
+ if (tokens[j + 1]?.value === 'as' && tokens[j + 2]?.type === 'name') {
93
+ local = tokens[j + 2].value;
94
+ j += 3;
95
+ }
96
+ else {
97
+ j++;
98
+ }
99
+ if (mod === 'django.utils.translation' && CANONICAL[imported])
100
+ names.set(local, CANONICAL[imported]);
101
+ if (mod === 'django.utils' && imported === 'translation')
102
+ modules.add(local);
103
+ if (tokens[j]?.value === ',')
104
+ j++;
105
+ else
106
+ break;
107
+ }
108
+ }
109
+ else if (t.value === 'import' && (i === 0 || tokens[i - 1].line !== t.line || tokens[i - 1].value !== 'from')) {
110
+ // import django.utils.translation as tr
111
+ let j = i + 1;
112
+ let mod = '';
113
+ while (j < tokens.length && (tokens[j].type === 'name' || tokens[j].value === '.') && tokens[j].value !== 'as' && tokens[j].line === t.line)
114
+ mod += tokens[j++].value;
115
+ if (mod === 'django.utils.translation') {
116
+ if (tokens[j]?.value === 'as' && tokens[j + 1]?.type === 'name')
117
+ modules.add(tokens[j + 1].value);
118
+ else
119
+ modules.add('django.utils.translation');
120
+ }
121
+ }
122
+ }
123
+ return { names, modules };
124
+ }
125
+ function resolveGettext(callee, aliases) {
126
+ if (!callee)
127
+ return null;
128
+ if (callee === '_')
129
+ return 'gettext';
130
+ const local = aliases.names.get(callee);
131
+ if (local)
132
+ return local;
133
+ const dot = callee.lastIndexOf('.');
134
+ if (dot > 0) {
135
+ const mod = callee.slice(0, dot);
136
+ const fn = callee.slice(dot + 1);
137
+ if (aliases.modules.has(mod) && CANONICAL[fn])
138
+ return CANONICAL[fn];
139
+ }
140
+ return null;
141
+ }
142
+ /** Dotted name ending at token index `end` (inclusive), e.g. `forms.ValidationError`. */
143
+ function dottedNameBefore(tokens, end) {
144
+ if (tokens[end]?.type !== 'name' || KEYWORDS.has(tokens[end].value))
145
+ return undefined;
146
+ const parts = [tokens[end].value];
147
+ let k = end - 1;
148
+ while (k >= 1 && tokens[k].value === '.' && tokens[k - 1].type === 'name') {
149
+ parts.unshift(tokens[k - 1].value);
150
+ k -= 2;
151
+ }
152
+ return parts.join('.');
153
+ }
154
+ /** A string that reads as words, not an identifier, path or code. */
155
+ function looksLikeNaturalLanguage(text) {
156
+ const t = text.trim();
157
+ if (t.length < 2 || !/\p{L}/u.test(t))
158
+ return false;
159
+ // A capitalized word ("Available", "E-book") reads as a label, not a code.
160
+ const capitalizedWord = /^\p{Lu}[\p{L}'-]*$/u.test(t) && /\p{Ll}/u.test(t) && !t.includes('_');
161
+ if (/^[\w.:/-]+$/.test(t) && !capitalizedWord)
162
+ return false; // snake_case, paths, urls, codes
163
+ if (/\.(?:html?|txt|css|js|py|json|xml|png|jpe?g|svg)$/i.test(t))
164
+ return false;
165
+ if (/^https?:\/\//.test(t))
166
+ return false;
167
+ return /\s/.test(t) || capitalizedWord || /^\p{Lu}\p{Ll}+[.!?]?$/u.test(t);
168
+ }
169
+ /** Name for an f-string field in `%(name)s` form. */
170
+ function fieldName(expr) {
171
+ const e = expr.trim();
172
+ let m = /^(?:[A-Za-z_]\w*\.)*([A-Za-z_]\w*)$/.exec(e);
173
+ if (m)
174
+ return m[1];
175
+ m = /^(?:[A-Za-z_]\w*\.)*(?:get_)?([A-Za-z_]\w*)\(\)$/.exec(e);
176
+ if (m)
177
+ return m[1];
178
+ m = /^len\(\s*(?:[A-Za-z_]\w*\.)*([A-Za-z_]\w*)\s*\)$/.exec(e);
179
+ if (m)
180
+ return `${m[1]}_count`;
181
+ m = /^(?:[A-Za-z_][\w.]*)\[\s*['"]([A-Za-z_]\w*)['"]\s*\]$/.exec(e);
182
+ if (m)
183
+ return m[1];
184
+ return null;
185
+ }
186
+ /**
187
+ * Value of an implicit-concatenation group of string tokens. Returns a
188
+ * string reason when the group can't be turned into a msgid.
189
+ */
190
+ function groupValue(src, tokens, first, last) {
191
+ const toks = tokens.slice(first, last + 1).filter((t) => t.type === 'string');
192
+ if (toks.some((t) => t.str.bytes))
193
+ return 'bytes literal';
194
+ const isF = toks.some((t) => t.str.f);
195
+ const original = src.slice(tokens[first].start, tokens[last].end);
196
+ if (!isF) {
197
+ const msgid = toks.map((t) => (0, python_lexer_1.decodePythonString)(src.slice(t.str.bodyStart, t.str.bodyEnd), t.str.raw)).join('');
198
+ return { msgid, inner: original, suffix: '', isF: false };
199
+ }
200
+ let msgid = '';
201
+ const fields = [];
202
+ for (const t of toks) {
203
+ const body = src.slice(t.str.bodyStart, t.str.bodyEnd);
204
+ if (!t.str.f) {
205
+ msgid += (0, python_lexer_1.decodePythonString)(body, t.str.raw).replace(/%/g, '%%');
206
+ continue;
207
+ }
208
+ const parts = (0, python_lexer_1.parseFString)(body, t.str.raw);
209
+ if (!parts)
210
+ return 'f-string with nested format fields';
211
+ for (const p of parts) {
212
+ if (p.kind === 'text') {
213
+ msgid += p.value.replace(/%/g, '%%');
214
+ continue;
215
+ }
216
+ if (p.debug)
217
+ return 'f-string with a self-documenting {expr=} field';
218
+ if (p.conversion && p.conversion !== 's')
219
+ return `f-string field with !${p.conversion} conversion`;
220
+ let spec = 's';
221
+ if (p.formatSpec !== undefined && p.formatSpec !== '') {
222
+ const sm = /^(\d*)(?:\.(\d+))?([dfs])$/.exec(p.formatSpec);
223
+ if (!sm)
224
+ return `f-string field with format spec ":${p.formatSpec}"`;
225
+ spec = `${sm[1]}${sm[2] !== undefined ? `.${sm[2]}` : ''}${sm[3]}`;
226
+ }
227
+ const base = fieldName(p.value);
228
+ if (!base)
229
+ return `f-string field {${p.value}} is too complex to name`;
230
+ const existing = fields.find((f) => f.expr === p.value);
231
+ let name = existing?.name;
232
+ if (!name) {
233
+ name = base;
234
+ for (let n = 2; fields.some((f) => f.name === name); n++)
235
+ name = `${base}${n}`;
236
+ fields.push({ name, expr: p.value });
237
+ }
238
+ msgid += `%(${name})${spec}`;
239
+ }
240
+ }
241
+ if (fields.length === 0) {
242
+ const plain = msgid.replace(/%%/g, '%');
243
+ return { msgid: plain, inner: (0, python_lexer_1.pythonStringLiteral)(plain), suffix: '', isF: true };
244
+ }
245
+ const dict = fields.map((f) => `${(0, python_lexer_1.pythonStringLiteral)(f.name)}: ${f.expr.replace(/\s*\n\s*/g, ' ')}`).join(', ');
246
+ return { msgid, inner: (0, python_lexer_1.pythonStringLiteral)(msgid), suffix: ` % {${dict}}`, isF: true };
247
+ }
248
+ function extractDjangoPython(src, options) {
249
+ const tokens = (0, python_lexer_1.tokenizePython)(src);
250
+ const lines = (0, python_lexer_1.logicalLines)(src, tokens);
251
+ const aliases = importAliases(tokens);
252
+ const isTemplateTags = /(^|[\\/])templatetags[\\/]/.test(options.filePath);
253
+ const findings = [];
254
+ const unwired = [];
255
+ const groups = [];
256
+ /** Function id → variable names passed as a user-facing message. */
257
+ const messageVars = new Map();
258
+ /** Token index → token position in the global array. */
259
+ const indexOf = new Map();
260
+ tokens.forEach((t, i) => indexOf.set(t, i));
261
+ const scope = [];
262
+ let nextScopeId = 1;
263
+ const ignoredLines = new Set();
264
+ for (const t of tokens)
265
+ if (t.type === 'comment' && /babelize[:-]\s*ignore/i.test(t.value))
266
+ ignoredLines.add(t.line);
267
+ for (const ln of lines) {
268
+ while (scope.length && scope[scope.length - 1].indent >= ln.indent)
269
+ scope.pop();
270
+ const toks = ln.tokens;
271
+ let headerKind = null;
272
+ let headerColon = -1;
273
+ const firstWord = toks[0].value === 'async' ? toks[1]?.value : toks[0].value;
274
+ if (firstWord === 'def' || firstWord === 'class') {
275
+ headerKind = firstWord;
276
+ let depth = 0;
277
+ for (let k = 0; k < toks.length; k++) {
278
+ const v = toks[k].value;
279
+ if (toks[k].type === 'op' && (v === '(' || v === '[' || v === '{'))
280
+ depth++;
281
+ else if (toks[k].type === 'op' && (v === ')' || v === ']' || v === '}'))
282
+ depth--;
283
+ else if (depth === 0 && v === ':' && toks[k].type === 'op') {
284
+ headerColon = k;
285
+ break;
286
+ }
287
+ }
288
+ }
289
+ const outerInFunction = scope.some((s) => s.kind === 'def');
290
+ const outerFunc = [...scope].reverse().find((s) => s.kind === 'def');
291
+ const outerClass = scope.length && scope[scope.length - 1].kind === 'class' ? scope[scope.length - 1] : undefined;
292
+ let headerFrame = null;
293
+ if (headerKind) {
294
+ let bases;
295
+ if (headerKind === 'class') {
296
+ const open = toks.findIndex((t) => t.value === '(');
297
+ if (open !== -1 && open < headerColon)
298
+ bases = src.slice(toks[open].end, toks[headerColon - 1].start);
299
+ }
300
+ headerFrame = { kind: headerKind, indent: ln.indent, id: nextScopeId++, bases };
301
+ }
302
+ const comments = ln.comments
303
+ .map((c) => c.value.replace(/^#\s?/, '').trim())
304
+ .filter((c) => /^translators\b/i.test(c));
305
+ // Walk the statement's tokens with a bracket stack.
306
+ const frames = [];
307
+ let assignTarget;
308
+ let assignAt = -1;
309
+ let topElemIndex = 0;
310
+ let topElemCount = 1;
311
+ const stmtGroups = [];
312
+ for (let k = 0; k < toks.length; k++) {
313
+ const tok = toks[k];
314
+ const gi = indexOf.get(tok);
315
+ const top = frames[frames.length - 1];
316
+ if (tok.type === 'op' && (tok.value === '(' || tok.value === '[' || tok.value === '{')) {
317
+ const prev = toks[k - 1];
318
+ const callable = prev && (prev.type === 'string' || (prev.type === 'name' && !KEYWORDS.has(prev.value)) || prev.value === ')' || prev.value === ']');
319
+ const kind = tok.value === '('
320
+ ? (callable ? 'call' : 'tuple')
321
+ : tok.value === '['
322
+ ? (callable ? 'subscript' : 'list')
323
+ : 'set';
324
+ const callee = kind === 'call' && prev?.type === 'name' ? dottedNameBefore(tokens, indexOf.get(prev)) : undefined;
325
+ const frame = {
326
+ open: tok.value, kind, callee, argIndex: 0, isDictValue: false, argStart: k + 1, args: new Map(),
327
+ parent: top, ownerKw: top?.kw, ownerArgIndex: top?.argIndex, ownerIsDictValue: top?.isDictValue,
328
+ ownerDictKey: top?.dictKey, ownerElemIndex: top ? top.argIndex : topElemIndex,
329
+ assignTarget: !top && assignAt !== -1 ? assignTarget : undefined, startTok: k,
330
+ };
331
+ // An empty `()` call still counts its single argument slot.
332
+ frames.push(frame);
333
+ continue;
334
+ }
335
+ if (tok.type === 'op' && (tok.value === ')' || tok.value === ']' || tok.value === '}')) {
336
+ const closing = frames.pop();
337
+ if (closing?.kind === 'call') {
338
+ const fn = resolveGettext(closing.callee, aliases);
339
+ if (fn)
340
+ emitWrapped(closing, fn, ln.comments);
341
+ }
342
+ continue;
343
+ }
344
+ if (tok.type === 'op' && tok.value === ',') {
345
+ if (top) {
346
+ top.argIndex++;
347
+ top.kw = undefined;
348
+ top.isDictValue = false;
349
+ top.dictKey = undefined;
350
+ top.argStart = k + 1;
351
+ }
352
+ else if (assignAt !== -1) {
353
+ topElemIndex++;
354
+ topElemCount++;
355
+ }
356
+ continue;
357
+ }
358
+ if (tok.type === 'op' && tok.value === ':' && top && (top.kind === 'set' || top.kind === 'dict') && !top.isDictValue) {
359
+ top.kind = 'dict';
360
+ top.isDictValue = true;
361
+ const keyToks = toks.slice(top.argStart, k);
362
+ if (keyToks.length > 0 && keyToks.every((t) => t.type === 'string' && !t.str.f && !t.str.bytes)) {
363
+ top.dictKey = keyToks.map((t) => (0, python_lexer_1.decodePythonString)(src.slice(t.str.bodyStart, t.str.bodyEnd), t.str.raw)).join('');
364
+ }
365
+ continue;
366
+ }
367
+ if (tok.type === 'op' && tok.value === '=') {
368
+ if (top?.kind === 'call' && k === top.argStart + 1 && toks[k - 1].type === 'name') {
369
+ top.kw = toks[k - 1].value;
370
+ top.argStart = k + 1;
371
+ }
372
+ else if (!top && assignAt === -1) {
373
+ assignAt = k;
374
+ // `x = …`, `self.x = …`, `x: int = …`
375
+ let end = k - 1;
376
+ const colon = toks.slice(0, k).findIndex((t) => t.value === ':');
377
+ if (colon !== -1)
378
+ end = colon - 1;
379
+ const target = toks.slice(0, end + 1);
380
+ if (target.length && target.every((t, idx) => (idx % 2 === 0 ? t.type === 'name' : t.value === '.'))) {
381
+ assignTarget = target.map((t) => t.value).join('');
382
+ }
383
+ topElemIndex = 0;
384
+ topElemCount = 1;
385
+ }
386
+ continue;
387
+ }
388
+ if (tok.type === 'string') {
389
+ // Implicit concatenation: consecutive string tokens form one value.
390
+ let last = k;
391
+ while (toks[last + 1]?.type === 'string')
392
+ last++;
393
+ const inBody = headerKind && headerColon !== -1 && k > headerColon;
394
+ const inFunction = outerInFunction || (!!inBody && headerKind === 'def');
395
+ const funcId = inBody && headerKind === 'def' ? headerFrame.id : outerFunc?.id ?? null;
396
+ const group = {
397
+ first: gi,
398
+ last: indexOf.get(toks[last]),
399
+ // Snapshot: frames keep mutating (argIndex, kw) as the walk goes on.
400
+ frames: frames.map((fr) => ({ ...fr })),
401
+ prev: toks[k - 1],
402
+ next: toks[last + 1],
403
+ nextNext: toks[last + 2],
404
+ assignTarget: frames.length === 0 && assignAt !== -1 ? assignTarget : undefined,
405
+ topElemIndex,
406
+ topElemCount,
407
+ statementKeyword: toks[0].type === 'name' ? toks[0].value : undefined,
408
+ inFunction,
409
+ funcId,
410
+ classBases: !inFunction ? (inBody && headerKind === 'class' ? headerFrame.bases : outerClass?.bases) : undefined,
411
+ inClass: !inFunction && ((!!inBody && headerKind === 'class') || !!outerClass),
412
+ line: tok.line,
413
+ comments,
414
+ };
415
+ // Whole-argument strings of a call are remembered for gettext extraction.
416
+ if (top?.kind === 'call' && top.argStart === k && (toks[last + 1]?.value === ',' || toks[last + 1]?.value === ')')) {
417
+ const v = groupValue(src, tokens, gi, group.last);
418
+ if (typeof v !== 'string' && !v.isF)
419
+ top.args.set(top.kw ?? top.argIndex, { value: v.msgid, line: tok.line });
420
+ }
421
+ stmtGroups.push(group);
422
+ k = last;
423
+ continue;
424
+ }
425
+ if (tok.type === 'name' && top?.kind === 'call' && top.argStart === k && toks[k + 1]?.value !== '(' && toks[k + 1]?.value !== '=') {
426
+ // `raise ValidationError(msg)` / `messages.info(request, msg.format(…))`
427
+ const fnId = inFunctionId();
428
+ if (fnId !== null && isMessageArg(top)) {
429
+ if (!messageVars.has(fnId))
430
+ messageVars.set(fnId, new Set());
431
+ messageVars.get(fnId).add(tok.value);
432
+ }
433
+ }
434
+ }
435
+ // Unclosed frames at the end of a line (shouldn't happen in valid code).
436
+ groups.push(...stmtGroups);
437
+ if (headerFrame)
438
+ scope.push(headerFrame);
439
+ function inFunctionId() {
440
+ if (headerKind === 'def' && headerColon !== -1)
441
+ return headerFrame.id;
442
+ return outerFunc?.id ?? null;
443
+ }
444
+ }
445
+ function emitWrapped(frame, fn, lineComments) {
446
+ const sig = SIGNATURES[fn];
447
+ const get = (pos, ...kws) => {
448
+ if (pos === undefined)
449
+ return undefined;
450
+ for (const kw of kws)
451
+ if (frame.args.has(kw))
452
+ return frame.args.get(kw);
453
+ return frame.args.get(pos);
454
+ };
455
+ const singular = get(sig.singular, 'message', 'singular', 'msgid');
456
+ if (!singular)
457
+ return; // dynamic msgid (a variable): nothing to extract
458
+ const plural = sig.plural !== undefined ? get(sig.plural, 'plural') : undefined;
459
+ const ctx = sig.ctx !== undefined ? get(sig.ctx, 'context') : undefined;
460
+ if (sig.plural !== undefined && !plural)
461
+ return;
462
+ if (sig.ctx !== undefined && !ctx)
463
+ return;
464
+ const message = {
465
+ msgid: singular.value,
466
+ msgidPlural: plural?.value,
467
+ msgctxt: ctx?.value,
468
+ flags: (0, types_1.formatFlags)(singular.value, plural?.value),
469
+ comments: lineComments.map((c) => c.value.replace(/^#\s?/, '').trim()).filter((c) => /^translators\b/i.test(c)),
470
+ };
471
+ if (!message.msgid)
472
+ return;
473
+ findings.push({ message, line: singular.line, wrapped: true });
474
+ }
475
+ function isMessageArg(frame) {
476
+ const callee = frame.callee ?? '';
477
+ if (MESSAGE_FUNCS.test(callee))
478
+ return frame.argIndex === 1 && !frame.kw || frame.kw === 'message';
479
+ if (callee === 'messages.add_message')
480
+ return frame.argIndex === 2 && !frame.kw || frame.kw === 'message';
481
+ if (/(?:^|\.)message_user$/.test(callee))
482
+ return frame.argIndex === 1 && !frame.kw || frame.kw === 'message';
483
+ if (/ValidationError$/.test(callee))
484
+ return frame.argIndex === 0 && !frame.kw || frame.kw === 'message';
485
+ return false;
486
+ }
487
+ /** Is this the root of a choices definition? */
488
+ function isChoicesRoot(f) {
489
+ if (!f)
490
+ return false;
491
+ if (f.parent === undefined && f.assignTarget && /(?:^|\.)[A-Z0-9_]*CHOICES$/i.test(f.assignTarget))
492
+ return true;
493
+ if (f.parent?.kind === 'call' && f.ownerKw === 'choices')
494
+ return true;
495
+ return false;
496
+ }
497
+ /** Why (if at all) a hardcoded string group should be wrapped. */
498
+ function wrapReason(g, text) {
499
+ const f = g.frames[g.frames.length - 1];
500
+ if (f?.kind === 'call') {
501
+ if (f.kw && UI_KWARGS.has(f.kw))
502
+ return 'ui';
503
+ if (f.kw === 'description' && /(?:^|\.)(?:action|display)$/.test(f.callee ?? ''))
504
+ return 'ui';
505
+ if (f.kw === 'message' && /(?:Validator|ValidationError)$/.test(f.callee ?? ''))
506
+ return 'ui';
507
+ if (isMessageArg(f))
508
+ return 'ui';
509
+ return null;
510
+ }
511
+ if (f?.kind === 'dict' && f.isDictValue) {
512
+ if (f.parent?.kind === 'call' && f.ownerKw === 'error_messages')
513
+ return 'ui';
514
+ if (!f.parent && f.assignTarget && /(?:^|\.)(?:default_)?error_messages$/.test(f.assignTarget))
515
+ return 'ui';
516
+ if (f.parent?.kind === 'call' && f.ownerKw === 'attrs' && f.dictKey && UI_ATTR_KEYS.has(f.dictKey))
517
+ return 'ui';
518
+ if (isChoicesRoot(f))
519
+ return 'ui';
520
+ if (isTemplateTags && looksLikeNaturalLanguage(text) && g.frames.length === 1)
521
+ return 'natural';
522
+ return null;
523
+ }
524
+ if (f && (f.kind === 'tuple' || f.kind === 'list')) {
525
+ // ("value", "Label") inside a choices list/tuple — or an optgroup label.
526
+ const container = f.parent;
527
+ const isLabel = f.kind === 'tuple' && f.argIndex === 1;
528
+ const isGroupLabel = f.kind === 'tuple' && f.argIndex === 0 && g.next?.value === ',' && (g.nextNext?.value === '[' || g.nextNext?.value === '(');
529
+ if (isLabel || isGroupLabel) {
530
+ let c = container;
531
+ // Walk up through optgroup nesting: list > tuple(elem 1) > list …
532
+ while (c) {
533
+ if (isChoicesRoot(c))
534
+ return 'ui';
535
+ if ((c.kind === 'list' || c.kind === 'tuple') && c.parent && (c.parent.kind === 'tuple' && c.ownerElemIndex === 1)) {
536
+ c = c.parent.parent;
537
+ continue;
538
+ }
539
+ break;
540
+ }
541
+ }
542
+ return null;
543
+ }
544
+ if (!f) {
545
+ // TextChoices members: NAME = "code", "Label"
546
+ if (g.classBases && /\b(?:TextChoices|IntegerChoices|Choices)\b/.test(g.classBases) && g.assignTarget &&
547
+ /^[A-Z][A-Z0-9_]*$/.test(g.assignTarget) && g.topElemIndex >= 1) {
548
+ return 'ui';
549
+ }
550
+ if (g.assignTarget && /(?:^|\.)short_description$/.test(g.assignTarget))
551
+ return 'ui';
552
+ // class Meta: verbose_name = "…" / AppConfig.verbose_name
553
+ if (g.inClass && g.assignTarget && /^verbose_name(?:_plural)?$/.test(g.assignTarget))
554
+ return 'ui';
555
+ if (isTemplateTags && g.inFunction && g.statementKeyword === 'return' && looksLikeNaturalLanguage(text))
556
+ return 'natural';
557
+ if (g.funcId !== null && g.assignTarget && messageVars.get(g.funcId)?.has(g.assignTarget) && looksLikeNaturalLanguage(text)) {
558
+ return 'natural';
559
+ }
560
+ }
561
+ return null;
562
+ }
563
+ const ALLOWED_PREV = new Set(['(', ',', '[', '{', '=', ':', 'return', 'else', 'yield']);
564
+ const ALLOWED_NEXT = new Set([')', ',', ']', '}', 'if', 'else', '%']);
565
+ for (const g of groups) {
566
+ if (ignoredLines.has(g.line))
567
+ continue;
568
+ // Strings inside a gettext call were handled as wrapped messages.
569
+ if (g.frames.some((f) => f.kind === 'call' && resolveGettext(f.callee, aliases)))
570
+ continue;
571
+ const value = groupValue(src, tokens, g.first, g.last);
572
+ const text = typeof value === 'string' ? src.slice(tokens[g.first].start, tokens[g.last].end) : value.msgid;
573
+ // Dict keys and the like are never text.
574
+ const f = g.frames[g.frames.length - 1];
575
+ if (f?.kind === 'dict' || f?.kind === 'set') {
576
+ if (!f.isDictValue || g.next?.value === ':')
577
+ continue;
578
+ }
579
+ if (g.next?.value === ':' && !(f && f.kind === 'subscript'))
580
+ continue;
581
+ if (f?.kind === 'subscript')
582
+ continue;
583
+ const reason = wrapReason(g, typeof value === 'string' ? '' : value.msgid);
584
+ if (!reason)
585
+ continue;
586
+ const prevOk = !g.prev || ALLOWED_PREV.has(g.prev.value);
587
+ const nextIsFormat = g.next?.value === '.' && g.nextNext?.value === 'format';
588
+ const nextOk = !g.next || ALLOWED_NEXT.has(g.next.value) || nextIsFormat;
589
+ if (typeof value === 'string') {
590
+ unwired.push({ line: g.line, text, reason: `not wrapped: ${value}` });
591
+ continue;
592
+ }
593
+ if (!/\p{L}/u.test(value.msgid) || !value.msgid.trim())
594
+ continue;
595
+ if (!prevOk || !nextOk) {
596
+ unwired.push({ line: g.line, text: value.msgid, reason: 'not wrapped: the string is part of a larger expression (concatenation or operator); wrap the whole sentence with gettext yourself' });
597
+ continue;
598
+ }
599
+ if (value.isF && (nextIsFormat || g.next?.value === '%')) {
600
+ unwired.push({ line: g.line, text: value.msgid, reason: 'not wrapped: f-string followed by further formatting' });
601
+ continue;
602
+ }
603
+ findings.push({
604
+ message: { msgid: value.msgid, flags: (0, types_1.formatFlags)(value.msgid), comments: g.comments },
605
+ line: g.line,
606
+ wrapped: false,
607
+ edit: {
608
+ kind: 'python',
609
+ start: tokens[g.first].start,
610
+ end: tokens[g.last].end,
611
+ lazy: !g.inFunction,
612
+ inner: value.inner,
613
+ suffix: value.suffix,
614
+ },
615
+ });
616
+ }
617
+ findings.sort((a, b) => a.line - b.line);
618
+ return { findings, unwired };
619
+ }