@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
@@ -1,33 +0,0 @@
1
- import type { TranslationTable } from 'gettext-parser';
2
- /**
3
- * Read a .po file content and return key-value pairs (msgid → msgstr).
4
- * For untranslated entries, msgstr will be an empty string.
5
- */
6
- export declare function readPoFile(content: string): Record<string, string>;
7
- /**
8
- * Read a .po file and return the full gettext-parser translation table
9
- * (preserves comments, msgctxt, plural forms, etc.)
10
- */
11
- export declare function readPoFileRaw(content: string): TranslationTable;
12
- /**
13
- * Write a .po file from key-value pairs.
14
- * Uses a gettext-parser translation table as the base for preserving
15
- * comments, msgctxt, and header metadata.
16
- */
17
- export declare function writePoFile(translations: Record<string, string>, options?: {
18
- context?: string;
19
- pluralForms?: string;
20
- baseContent?: string;
21
- }): string;
22
- /**
23
- * Compile a .po file to .mo binary format.
24
- */
25
- export declare function compileMo(poContent: string): Buffer;
26
- /**
27
- * Build a gettext-parser table from flat translation entries.
28
- * Useful when generating a fresh .po file from extracted keys.
29
- */
30
- export declare function buildPoTable(translations: Record<string, string>, options?: {
31
- sourceLanguage?: string;
32
- pluralForms?: string;
33
- }): TranslationTable;
@@ -1,131 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.readPoFile = readPoFile;
7
- exports.readPoFileRaw = readPoFileRaw;
8
- exports.writePoFile = writePoFile;
9
- exports.compileMo = compileMo;
10
- exports.buildPoTable = buildPoTable;
11
- const gettext_parser_1 = __importDefault(require("gettext-parser"));
12
- /**
13
- * Read a .po file content and return key-value pairs (msgid → msgstr).
14
- * For untranslated entries, msgstr will be an empty string.
15
- */
16
- function readPoFile(content) {
17
- const parsed = gettext_parser_1.default.po.parse(content);
18
- const result = {};
19
- for (const context of Object.values(parsed.translations)) {
20
- for (const [msgid, entry] of Object.entries(context)) {
21
- if (!msgid)
22
- continue; // skip header entry
23
- const msgstr = entry.msgstr?.[0] || '';
24
- result[msgid] = msgstr;
25
- }
26
- }
27
- return result;
28
- }
29
- /**
30
- * Read a .po file and return the full gettext-parser translation table
31
- * (preserves comments, msgctxt, plural forms, etc.)
32
- */
33
- function readPoFileRaw(content) {
34
- return gettext_parser_1.default.po.parse(content);
35
- }
36
- /**
37
- * Write a .po file from key-value pairs.
38
- * Uses a gettext-parser translation table as the base for preserving
39
- * comments, msgctxt, and header metadata.
40
- */
41
- function writePoFile(translations, options) {
42
- const ctx = options?.context || '';
43
- // Build the gettext-parser table structure
44
- const table = {
45
- charset: 'utf-8',
46
- headers: {
47
- 'content-type': 'text/plain; charset=utf-8',
48
- 'plural-forms': options?.pluralForms || 'nplurals=2; plural=(n!=1);',
49
- },
50
- translations: {
51
- [ctx]: {},
52
- },
53
- };
54
- // If base content is provided, merge existing metadata
55
- if (options?.baseContent) {
56
- const base = gettext_parser_1.default.po.parse(options.baseContent);
57
- table.headers = { ...base.headers, ...table.headers };
58
- if (base.charset)
59
- table.charset = base.charset;
60
- // Preserve existing entries for context
61
- if (base.translations[ctx]) {
62
- for (const [msgid, entry] of Object.entries(base.translations[ctx])) {
63
- if (!msgid)
64
- continue;
65
- table.translations[ctx][msgid] = entry;
66
- }
67
- }
68
- }
69
- // Apply translations
70
- for (const [msgid, msgstr] of Object.entries(translations)) {
71
- if (!msgid)
72
- continue;
73
- if (table.translations[ctx][msgid]) {
74
- table.translations[ctx][msgid].msgstr = [msgstr];
75
- }
76
- else {
77
- table.translations[ctx][msgid] = {
78
- msgid,
79
- msgstr: [msgstr],
80
- };
81
- }
82
- }
83
- const compiled = gettext_parser_1.default.po.compile(table, {
84
- foldLength: 0,
85
- sort: false,
86
- escapeCharacters: true,
87
- });
88
- return compiled.toString('utf-8');
89
- }
90
- /**
91
- * Compile a .po file to .mo binary format.
92
- */
93
- function compileMo(poContent) {
94
- const parsed = gettext_parser_1.default.po.parse(poContent);
95
- return gettext_parser_1.default.mo.compile(parsed);
96
- }
97
- /**
98
- * Build a gettext-parser table from flat translation entries.
99
- * Useful when generating a fresh .po file from extracted keys.
100
- */
101
- function buildPoTable(translations, options) {
102
- const ctx = '';
103
- const headers = {
104
- 'content-type': 'text/plain; charset=utf-8',
105
- 'plural-forms': options?.pluralForms || 'nplurals=2; plural=(n!=1);',
106
- };
107
- if (options?.sourceLanguage) {
108
- headers['language'] = options.sourceLanguage;
109
- }
110
- const table = {
111
- charset: 'utf-8',
112
- headers,
113
- translations: {
114
- [ctx]: {},
115
- },
116
- };
117
- // Add header entry
118
- table.translations[ctx][''] = {
119
- msgid: '',
120
- msgstr: [Object.entries(headers).map(([k, v]) => `${k}: ${v}`).join('\n')],
121
- };
122
- for (const [msgid, msgstr] of Object.entries(translations)) {
123
- if (!msgid)
124
- continue;
125
- table.translations[ctx][msgid] = {
126
- msgid,
127
- msgstr: [msgstr],
128
- };
129
- }
130
- return table;
131
- }
@@ -1,5 +0,0 @@
1
- import type { BabelizeConfig, TranslationResult, TranslationUnit, TranslatorClient } from '../../types';
2
- export interface TranslatorOptions {
3
- client: TranslatorClient;
4
- }
5
- export declare function translateUnits(config: BabelizeConfig, units: TranslationUnit[], opts: TranslatorOptions): Promise<TranslationResult[]>;
@@ -1,46 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.translateUnits = translateUnits;
4
- const glossary_1 = require("../glossary");
5
- const language_mapper_1 = require("./language-codes/language-mapper");
6
- const batch_optimizer_1 = require("./batch-optimizer");
7
- async function translateUnits(config, units, opts) {
8
- if (!units.length)
9
- return [];
10
- const sourceFlores = (0, language_mapper_1.toFlores)(config.sourceLocale);
11
- const results = [];
12
- const glossaryTerms = config.glossary?.neverTranslate ?? [];
13
- const batches = (0, batch_optimizer_1.createBatches)(units);
14
- // Sequential batches — NEVER parallel on HF free tier.
15
- for (const batch of batches) {
16
- const payload = batch.units.map((u) => {
17
- const { masked, terms } = (0, glossary_1.applyGlossaryMask)(u.sourceText, glossaryTerms);
18
- return { unit: u, masked, terms };
19
- });
20
- for (const { unit, masked, terms } of payload) {
21
- const targetFlores = (0, language_mapper_1.toFlores)(unit.targetLocale);
22
- const res = await opts.client.translate({
23
- entries: [{ key: 'main', value: masked }],
24
- sourceLang: sourceFlores,
25
- targetLang: targetFlores,
26
- });
27
- if (!Array.isArray(res.translations) || res.translations.length === 0) {
28
- throw new Error('HF translator returned no translations');
29
- }
30
- const translatedText = res.translations[0].value;
31
- if (typeof translatedText !== 'string') {
32
- throw new Error('HF translator returned an invalid non-string translation value');
33
- }
34
- const restored = (0, glossary_1.restoreGlossaryMask)(translatedText, terms);
35
- results.push({
36
- sourceText: unit.sourceText,
37
- sourceLocale: unit.sourceLocale,
38
- targetLocale: unit.targetLocale,
39
- translatedText: restored,
40
- provider: 'hf',
41
- model: res.model,
42
- });
43
- }
44
- }
45
- return results;
46
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Mask Django Template Language tags with private-use sentinels
3
- * so node-html-parser treats them as opaque text.
4
- *
5
- * Handles: {% %}, {{ }}, {# #}
6
- *
7
- * After masking, the HTML can be safely parsed by node-html-parser
8
- * and run through the standard HTML injection pipeline.
9
- */
10
- /**
11
- * Mask DTL template tags in content.
12
- * Returns the masked string and a restore function to undo masking.
13
- */
14
- export declare function maskTemplateTags(content: string): {
15
- masked: string;
16
- restore: (masked: string) => string;
17
- };
18
- /**
19
- * Check if content contains any Django template tags.
20
- */
21
- export declare function hasTemplateTags(content: string): boolean;
22
- /**
23
- * Extract all DTL template tags from content for inspection.
24
- */
25
- export declare function extractTemplateTags(content: string): string[];
@@ -1,58 +0,0 @@
1
- "use strict";
2
- /**
3
- * Mask Django Template Language tags with private-use sentinels
4
- * so node-html-parser treats them as opaque text.
5
- *
6
- * Handles: {% %}, {{ }}, {# #}
7
- *
8
- * After masking, the HTML can be safely parsed by node-html-parser
9
- * and run through the standard HTML injection pipeline.
10
- */
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.maskTemplateTags = maskTemplateTags;
13
- exports.hasTemplateTags = hasTemplateTags;
14
- exports.extractTemplateTags = extractTemplateTags;
15
- // Unicode Private Use Area block tag characters used as sentinels
16
- const BLOCK_START = '\uE000';
17
- const BLOCK_END = '\uE001';
18
- /**
19
- * Mask DTL template tags in content.
20
- * Returns the masked string and a restore function to undo masking.
21
- */
22
- function maskTemplateTags(content) {
23
- const replacements = [];
24
- let counter = 0;
25
- // Unified pattern for {% %}, {{ }}, {# #}
26
- // Match opening braces with space-or-tag or closing braces with space-or-tag
27
- const tagPattern = /(\{%.*?%\}|\{\{.*?\}\}|\{#.*?#\})/gs;
28
- const masked = content.replace(tagPattern, (match) => {
29
- const sentinel = `${BLOCK_START}${counter}${BLOCK_END}`;
30
- replacements.push({ sentinel, original: match });
31
- counter++;
32
- return sentinel;
33
- });
34
- return {
35
- masked,
36
- restore: (maskedContent) => {
37
- let restored = maskedContent;
38
- for (const { sentinel, original } of replacements) {
39
- // Escape sentinel for safe use in regex replacement
40
- restored = restored.split(sentinel).join(original);
41
- }
42
- return restored;
43
- },
44
- };
45
- }
46
- /**
47
- * Check if content contains any Django template tags.
48
- */
49
- function hasTemplateTags(content) {
50
- return /\{%.*?%\}|\{\{.*?\}\}|\{#.*?#\}/s.test(content);
51
- }
52
- /**
53
- * Extract all DTL template tags from content for inspection.
54
- */
55
- function extractTemplateTags(content) {
56
- const tagPattern = /\{%.*?%\}|\{\{.*?\}\}|\{#.*?#\}/gs;
57
- return content.match(tagPattern) || [];
58
- }
@@ -1,14 +0,0 @@
1
- import type { ExtractedString, Warning } from '../runtime/types';
2
- /**
3
- * Extract translatable strings from Python source code.
4
- *
5
- * Matches: _(), gettext(), gettext_lazy(), ugettext*(), ngettext()
6
- * Also opt-in: verbose_name=, help_text=, label= kwargs
7
- *
8
- * Uses a character-position lexer approach (not a full AST parser)
9
- * to handle single/double/triple quotes, raw strings, f-strings, and escape sequences.
10
- */
11
- export declare function extractStringsFromPython(content: string, filePath: string): {
12
- strings: ExtractedString[];
13
- warnings: Warning[];
14
- };
@@ -1,285 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractStringsFromPython = extractStringsFromPython;
4
- const string_utils_1 = require("./string-utils");
5
- // Function calls that indicate translatable strings
6
- const GETTEXT_CALLS = [
7
- '_',
8
- 'gettext',
9
- 'gettext_lazy',
10
- 'ngettext',
11
- 'ngettext_lazy',
12
- 'ugettext',
13
- 'ugettext_lazy',
14
- 'pgettext',
15
- 'pgettext_lazy',
16
- 'npgettext',
17
- 'npgettext_lazy',
18
- ];
19
- // Keyword argument names that Django/Python treat as translatable
20
- const TRANSlatable_KWARGS = [
21
- 'verbose_name',
22
- 'verbose_name_plural',
23
- 'help_text',
24
- 'label',
25
- ];
26
- /**
27
- * Extract translatable strings from Python source code.
28
- *
29
- * Matches: _(), gettext(), gettext_lazy(), ugettext*(), ngettext()
30
- * Also opt-in: verbose_name=, help_text=, label= kwargs
31
- *
32
- * Uses a character-position lexer approach (not a full AST parser)
33
- * to handle single/double/triple quotes, raw strings, f-strings, and escape sequences.
34
- */
35
- function extractStringsFromPython(content, filePath) {
36
- const ctx = {
37
- filePath,
38
- content,
39
- strings: [],
40
- seenTexts: new Set(),
41
- warnings: [],
42
- };
43
- let i = 0;
44
- const len = content.length;
45
- while (i < len) {
46
- // Skip lines that are comments
47
- if (content[i] === '#') {
48
- i = skipToNextLine(content, i);
49
- continue;
50
- }
51
- // Skip string literals on their own (not preceded by a function call or kwarg)
52
- // We only extract strings that are arguments to gettext functions or Django kwargs
53
- // Check for gettext function calls: _( "..." ), gettext( "..." ), etc.
54
- for (const callName of GETTEXT_CALLS) {
55
- const callPattern = new RegExp(`\\b${callName.replace(/_/g, '\\_')}\\s*\\(`);
56
- const match = callPattern.exec(content.slice(i));
57
- if (match && match.index === 0) {
58
- const parenStart = i + match[0].length;
59
- const args = collectCallArgs(content, parenStart);
60
- if (args) {
61
- for (const arg of args) {
62
- const str = extractStringFromArg(content, arg);
63
- if (str !== null) {
64
- addString(ctx, str.text, arg.startLine, 0, str.context);
65
- }
66
- }
67
- }
68
- i = args ? (args[args.length - 1]?.end || parenStart) : parenStart;
69
- break;
70
- }
71
- }
72
- // Check for Django keyword arguments: verbose_name="...", help_text="..."
73
- for (const kwarg of TRANSlatable_KWARGS) {
74
- const kwargPattern = new RegExp(`\\b${kwarg}\\s*=\\s*`);
75
- const match = kwargPattern.exec(content.slice(i));
76
- if (match && match.index === 0) {
77
- const valueStart = i + match[0].length;
78
- const str = extractStringLiteral(content, valueStart);
79
- if (str !== null) {
80
- addString(ctx, str.text, findLineNumber(content, valueStart), 0, str.context);
81
- }
82
- i = valueStart + (str ? str.rawLength : 0);
83
- break;
84
- }
85
- }
86
- i++;
87
- }
88
- return { strings: ctx.strings, warnings: ctx.warnings };
89
- }
90
- /**
91
- * Collect argument ranges for a function call starting after the opening paren.
92
- * Returns an array of {start, end, startLine} for each argument position.
93
- */
94
- function collectCallArgs(content, start) {
95
- let depth = 1;
96
- let i = start;
97
- const args = [];
98
- let argStart = -1;
99
- let parenDepth = 0;
100
- while (i < content.length && depth > 0) {
101
- const ch = content[i];
102
- if (ch === '(') {
103
- parenDepth++;
104
- }
105
- else if (ch === ')') {
106
- parenDepth--;
107
- if (parenDepth === 0 && depth === 1) {
108
- if (argStart !== -1) {
109
- args.push({
110
- start: argStart,
111
- end: i,
112
- startLine: findLineNumber(content, argStart),
113
- });
114
- }
115
- depth--;
116
- i++;
117
- continue;
118
- }
119
- }
120
- else if (ch === ',' && parenDepth === 1) {
121
- if (argStart !== -1) {
122
- args.push({
123
- start: argStart,
124
- end: i,
125
- startLine: findLineNumber(content, argStart),
126
- });
127
- }
128
- argStart = -1;
129
- }
130
- else if (ch === '"' || ch === "'" || ch === 'f') {
131
- // String literal — skip over it
132
- const strEnd = skipString(content, i);
133
- if (strEnd !== i) {
134
- if (argStart === -1)
135
- argStart = i;
136
- i = strEnd;
137
- continue;
138
- }
139
- }
140
- // Track first non-whitespace as arg start
141
- if (argStart === -1 && !/\s/.test(ch) && ch !== ',') {
142
- // Check if this is a keyword arg or string
143
- if (ch !== ')') {
144
- argStart = i;
145
- }
146
- }
147
- i++;
148
- }
149
- return args.length > 0 ? args : null;
150
- }
151
- /**
152
- * Extract the string value from an argument position.
153
- * Handles named arguments (_("text"), _("text", count)).
154
- * Only extracts the first string argument (singular form).
155
- */
156
- function extractStringFromArg(content, arg) {
157
- // Skip keyword name if present (e.g., msgid="text")
158
- let i = arg.start;
159
- const end = arg.end;
160
- while (i < end && content[i] !== '"' && content[i] !== "'" && content[i] !== 'f') {
161
- i++;
162
- }
163
- if (i >= end)
164
- return null;
165
- const str = extractStringLiteral(content, i);
166
- if (!str)
167
- return null;
168
- return {
169
- text: str.text,
170
- rawLength: str.rawLength,
171
- context: str.context,
172
- };
173
- }
174
- /**
175
- * Extract a string literal from content starting at the given position.
176
- * Handles: "...", '...', """...""", '''...''', f"...", r"...", b"..."
177
- */
178
- function extractStringLiteral(content, start) {
179
- let i = start;
180
- const len = content.length;
181
- // Skip prefix characters: r, f, b, rb, fr, etc.
182
- while (i < len && /[a-zA-Z]/.test(content[i]) && content[i] !== '"' && content[i] !== "'") {
183
- i++;
184
- }
185
- if (i >= len)
186
- return null;
187
- const quote = content[i];
188
- if (quote !== '"' && quote !== "'")
189
- return null;
190
- // Check for triple-quote
191
- const isTriple = content.slice(i, i + 3) === quote.repeat(3);
192
- const openQuote = isTriple ? quote.repeat(3) : quote;
193
- const closeQuote = openQuote;
194
- const valueStart = i + openQuote.length;
195
- let j = valueStart;
196
- let text = '';
197
- let context = 'string-literal';
198
- // Detect f-string
199
- const prefixStart = start;
200
- const prefix = content.slice(prefixStart, i).toLowerCase();
201
- if (prefix.includes('f')) {
202
- context = 'template-literal';
203
- }
204
- while (j < len) {
205
- if (content[j] === '\\') {
206
- // Escape sequence — skip next char
207
- j += 2;
208
- continue;
209
- }
210
- if (content.slice(j, j + closeQuote.length) === closeQuote) {
211
- // Found closing quote
212
- const rawLength = j + closeQuote.length - start;
213
- return { text, rawLength, context };
214
- }
215
- text += content[j];
216
- j++;
217
- }
218
- return null;
219
- }
220
- /**
221
- * Skip over a string literal starting at the given position.
222
- * Returns the index after the closing quote.
223
- */
224
- function skipString(content, start) {
225
- let i = start;
226
- const len = content.length;
227
- // Skip prefix characters
228
- while (i < len && /[a-zA-Z]/.test(content[i]) && content[i] !== '"' && content[i] !== "'") {
229
- i++;
230
- }
231
- if (i >= len)
232
- return start;
233
- const quote = content[i];
234
- if (quote !== '"' && quote !== "'")
235
- return start;
236
- const isTriple = content.slice(i, i + 3) === quote.repeat(3);
237
- const closeQuote = isTriple ? quote.repeat(3) : quote;
238
- i += closeQuote.length;
239
- while (i < len) {
240
- if (content[i] === '\\') {
241
- i += 2;
242
- continue;
243
- }
244
- if (content.slice(i, i + closeQuote.length) === closeQuote) {
245
- return i + closeQuote.length;
246
- }
247
- i++;
248
- }
249
- return start; // Unterminated string
250
- }
251
- /**
252
- * Add a string to the extraction context, deduplicating by content hash.
253
- */
254
- function addString(ctx, text, line, column, context) {
255
- const trimmed = text.trim();
256
- if (!trimmed || trimmed.length < 2)
257
- return;
258
- if (!(0, string_utils_1.isTranslatableText)(trimmed))
259
- return;
260
- const contentKey = `${ctx.filePath}:${(0, string_utils_1.hashString)(trimmed)}`;
261
- if (ctx.seenTexts.has(contentKey))
262
- return;
263
- const id = `${ctx.filePath}:${line}:${(0, string_utils_1.hashString)(trimmed)}`;
264
- ctx.seenTexts.add(contentKey);
265
- ctx.strings.push({
266
- text: trimmed,
267
- file: ctx.filePath,
268
- line,
269
- column,
270
- context,
271
- id,
272
- });
273
- }
274
- function skipToNextLine(content, pos) {
275
- const newline = content.indexOf('\n', pos);
276
- return newline === -1 ? content.length : newline + 1;
277
- }
278
- function findLineNumber(content, pos) {
279
- let line = 1;
280
- for (let i = 0; i < pos && i < content.length; i++) {
281
- if (content[i] === '\n')
282
- line++;
283
- }
284
- return line;
285
- }