i18ntk 3.0.0 → 3.1.0

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.
@@ -131,13 +131,14 @@ class SettingsCLI {
131
131
  { key: '1', label: t('settings.mainMenu.uiSettings'), description: t('settings.mainMenu.uiSettingsDesc') },
132
132
  { key: '2', label: t('settings.mainMenu.directorySettings'), description: t('settings.mainMenu.directorySettingsDesc') },
133
133
  { key: '3', label: t('settings.mainMenu.scriptDirectorySettings'), description: t('settings.mainMenu.scriptDirectorySettingsDesc') },
134
- { key: '4', label: t('settings.mainMenu.processingSettings'), description: t('settings.mainMenu.processingSettingsDesc') },
135
- { key: '5', label: t('settings.mainMenu.backupSettings'), description: t('settings.mainMenu.backupSettingsDesc') },
136
- { key: '6', label: t('settings.mainMenu.securitySettings'), description: `${t('settings.mainMenu.securitySettingsDesc')} ${pinStatus}` },
137
- { key: '7', label: t('settings.mainMenu.advancedSettings'), description: t('settings.mainMenu.advancedSettingsDesc') },
138
- { key: '8', label: t('settings.mainMenu.viewAllSettings'), description: t('settings.mainMenu.viewAllSettingsDesc') },
139
- { key: '9', label: t('settings.mainMenu.importExport'), description: t('settings.mainMenu.importExportDesc') },
140
- { key: '0', label: t('settings.mainMenu.reportBug'), description: t('settings.mainMenu.reportBugDesc') },
134
+ { key: '4', label: t('settings.mainMenu.processingSettings'), description: t('settings.mainMenu.processingSettingsDesc') },
135
+ { key: '5', label: t('settings.mainMenu.backupSettings'), description: t('settings.mainMenu.backupSettingsDesc') },
136
+ { key: '6', label: t('settings.mainMenu.securitySettings'), description: `${t('settings.mainMenu.securitySettingsDesc')} ${pinStatus}` },
137
+ { key: '7', label: t('settings.mainMenu.advancedSettings'), description: t('settings.mainMenu.advancedSettingsDesc') },
138
+ { key: '8', label: t('settings.mainMenu.viewAllSettings'), description: t('settings.mainMenu.viewAllSettingsDesc') },
139
+ { key: '9', label: t('settings.mainMenu.importExport'), description: t('settings.mainMenu.importExportDesc') },
140
+ { key: 'a', label: 'Auto Translate Beta', description: 'Tune placeholder handling, concurrency, batching, and reports' },
141
+ { key: '0', label: t('settings.mainMenu.reportBug'), description: t('settings.mainMenu.reportBugDesc') },
141
142
  { key: 'x', label: 'Reset Script Directory Overrides', description: 'Clear script directory overrides and use defaults' },
142
143
  { key: 'r', label: t('settings.mainMenu.resetToDefaults'), description: t('settings.mainMenu.resetToDefaultsDesc') },
143
144
  { key: 'u', label: t('settings.mainMenu.updatePackage'), description: t('settings.mainMenu.updatePackageDesc') },
@@ -188,12 +189,15 @@ class SettingsCLI {
188
189
  case '8':
189
190
  await this.showAllSettings();
190
191
  break;
191
- case '9':
192
- await this.showImportExport();
193
- break;
194
- case '0':
195
- await this.reportBug();
196
- break;
192
+ case '9':
193
+ await this.showImportExport();
194
+ break;
195
+ case 'a':
196
+ await this.showAutoTranslateSettings();
197
+ break;
198
+ case '0':
199
+ await this.reportBug();
200
+ break;
197
201
  case 'x':
198
202
  await this.resetScriptDirectories();
199
203
  break;
@@ -302,7 +306,7 @@ class SettingsCLI {
302
306
  /**
303
307
  * Show processing settings menu
304
308
  */
305
- async showProcessingSettings() {
309
+ async showProcessingSettings() {
306
310
  // Refresh language from settings to ensure consistency
307
311
  if (typeof uiI18n.refreshLanguageFromSettings === 'function') {
308
312
  uiI18n.refreshLanguageFromSettings();
@@ -318,8 +322,37 @@ class SettingsCLI {
318
322
  'advanced.sizingThreshold': t('settings.fields.sizingThreshold.label')
319
323
  };
320
324
 
321
- await this.showSettingsCategory(processSettings);
322
- }
325
+ await this.showSettingsCategory(processSettings);
326
+ }
327
+
328
+ async showAutoTranslateSettings() {
329
+ if (typeof uiI18n.refreshLanguageFromSettings === 'function') {
330
+ uiI18n.refreshLanguageFromSettings();
331
+ }
332
+
333
+ this.clearScreen();
334
+ this.showHeader();
335
+ console.log(`${colors.bright}Auto Translate Beta Settings${colors.reset}\n`);
336
+
337
+ const autoTranslateSettings = {
338
+ 'autoTranslate.placeholderMode': 'Placeholder handling mode',
339
+ 'autoTranslate.concurrency': 'Concurrent translation requests',
340
+ 'autoTranslate.batchSize': 'Text segments per batch',
341
+ 'autoTranslate.progressInterval': 'Progress update interval',
342
+ 'autoTranslate.retryCount': 'Retry count',
343
+ 'autoTranslate.retryDelay': 'Retry delay (ms)',
344
+ 'autoTranslate.timeout': 'Request timeout (ms)',
345
+ 'autoTranslate.dryRunFirst': 'Dry-run preview first',
346
+ 'autoTranslate.reportStdout': 'Print report to terminal',
347
+ 'autoTranslate.bom': 'Write UTF-8 BOM',
348
+ 'autoTranslate.protectionEnabled': 'Protect brand terms, keys, values',
349
+ 'autoTranslate.protectionFile': 'Protection JSON file',
350
+ 'autoTranslate.promptProtectionSetup': 'Ask to create protection file',
351
+ 'autoTranslate.promptProtectionUpdate': 'Ask to update protection rules'
352
+ };
353
+
354
+ await this.showSettingsCategory(autoTranslateSettings);
355
+ }
323
356
 
324
357
  /**
325
358
  * Show advanced settings menu
@@ -531,9 +564,16 @@ class SettingsCLI {
531
564
  'security.adminPinEnabled': ['true', 'false'],
532
565
  'security.pinProtection.enabled': ['true', 'false'],
533
566
  'advanced.backupBeforeChanges': ['true', 'false'],
534
- 'backup.enabled': ['true', 'false'],
535
- 'backup.singleFileMode': ['true', 'false']
536
- };
567
+ 'backup.enabled': ['true', 'false'],
568
+ 'backup.singleFileMode': ['true', 'false'],
569
+ 'autoTranslate.placeholderMode': ['preserve', 'skip', 'send'],
570
+ 'autoTranslate.dryRunFirst': ['true', 'false'],
571
+ 'autoTranslate.reportStdout': ['true', 'false'],
572
+ 'autoTranslate.bom': ['true', 'false'],
573
+ 'autoTranslate.protectionEnabled': ['true', 'false'],
574
+ 'autoTranslate.promptProtectionSetup': ['true', 'false'],
575
+ 'autoTranslate.promptProtectionUpdate': ['true', 'false']
576
+ };
537
577
  if (key === 'language') {
538
578
  return settingsManager.getAvailableLanguages().map(l => l.code);
539
579
  }
@@ -566,8 +606,14 @@ class SettingsCLI {
566
606
  'lockoutDuration': { min: 1, max: 60, type: 'int', unit: 'minutes' },
567
607
  'backupRetention': { min: 1, max: 30, type: 'int', unit: 'days' },
568
608
  'logRetention': { min: 1, max: 90, type: 'int', unit: 'days' },
569
- 'retentionDays': { min: 1, max: 365, type: 'int', unit: 'days' },
570
- 'maxBackups': { min: 1, max: 3, type: 'int' }
609
+ 'retentionDays': { min: 1, max: 365, type: 'int', unit: 'days' },
610
+ 'maxBackups': { min: 1, max: 3, type: 'int' },
611
+ 'autoTranslate.concurrency': { min: 1, max: 25, type: 'int' },
612
+ 'autoTranslate.batchSize': { min: 1, max: 10000, type: 'int' },
613
+ 'autoTranslate.progressInterval': { min: 1, max: 10000, type: 'int' },
614
+ 'autoTranslate.retryCount': { min: 0, max: 10, type: 'int' },
615
+ 'autoTranslate.retryDelay': { min: 0, max: 30000, type: 'int', unit: 'ms' },
616
+ 'autoTranslate.timeout': { min: 1000, max: 120000, type: 'int', unit: 'ms' }
571
617
  };
572
618
 
573
619
  for (const [field, rules] of Object.entries(validations)) {
@@ -827,10 +873,10 @@ class SettingsCLI {
827
873
  let finalValue = convertedValue;
828
874
  if (typeof finalValue === 'string') {
829
875
  const lowerKey = key.toLowerCase();
830
- if (lowerKey.includes('dir') || lowerKey.includes('path') || lowerKey.includes('root')) {
831
- finalValue = finalValue.replace(/^([/\\])/, './');
832
- finalValue = configManager.toRelative(path.resolve(finalValue));
833
- }
876
+ if (lowerKey.includes('dir') || lowerKey.includes('path') || lowerKey.includes('root') || key === 'autoTranslate.protectionFile') {
877
+ finalValue = finalValue.replace(/^([/\\])/, './');
878
+ finalValue = configManager.toRelative(path.resolve(finalValue));
879
+ }
834
880
  }
835
881
  this.setNestedValue(this.settings, key, finalValue);
836
882
  this.modified = true;
@@ -1934,9 +1980,9 @@ ${colors.dim}${t('settings.updatePackage.command')}: npm update i18ntk -g${color
1934
1980
  return `${colors.dim}(not set)${colors.reset}`;
1935
1981
  }
1936
1982
  const lowerKey = key.toLowerCase();
1937
- if (typeof value === 'string' && (lowerKey.includes('dir') || lowerKey.includes('path') || lowerKey.includes('root'))) {
1938
- return configManager.toRelative(path.resolve(value));
1939
- }
1983
+ if (typeof value === 'string' && (lowerKey.includes('dir') || lowerKey.includes('path') || lowerKey.includes('root') || key === 'autoTranslate.protectionFile')) {
1984
+ return configManager.toRelative(path.resolve(value));
1985
+ }
1940
1986
  if (typeof value === 'boolean') {
1941
1987
  return value ? `${colors.green}enabled${colors.reset}` : `${colors.red}disabled${colors.reset}`;
1942
1988
  }
@@ -1960,7 +2006,7 @@ ${colors.dim}${t('settings.updatePackage.command')}: npm update i18ntk -g${color
1960
2006
  if (['false','no','n','0','off','disabled'].includes(lower)) return false;
1961
2007
 
1962
2008
  // Numbers for known numeric settings
1963
- const numericKeys = /(sessionTimeout|maxFailedAttempts|lockoutDuration|retentionDays|maxBackups|advanced\.|backup\.)/;
2009
+ const numericKeys = /(sessionTimeout|maxFailedAttempts|lockoutDuration|retentionDays|maxBackups|advanced\.|backup\.|autoTranslate\.(concurrency|batchSize|progressInterval|retryCount|retryDelay|timeout))/;
1964
2010
  if (numericKeys.test(key)) {
1965
2011
  const n = Number(raw);
1966
2012
  if (!Number.isNaN(n)) return n;
@@ -52,6 +52,22 @@ class SettingsManager {
52
52
  "memoryOptimization": true,
53
53
  "compression": true
54
54
  },
55
+ "autoTranslate": {
56
+ "placeholderMode": "preserve",
57
+ "concurrency": 6,
58
+ "batchSize": 100,
59
+ "progressInterval": 25,
60
+ "retryCount": 3,
61
+ "retryDelay": 1000,
62
+ "timeout": 15000,
63
+ "dryRunFirst": true,
64
+ "reportStdout": true,
65
+ "bom": false,
66
+ "protectionEnabled": true,
67
+ "protectionFile": "./i18ntk-auto-translate.json",
68
+ "promptProtectionSetup": true,
69
+ "promptProtectionUpdate": true
70
+ },
55
71
  "reports": {
56
72
  "format": "json",
57
73
  "includeSource": false,
@@ -368,6 +384,10 @@ class SettingsManager {
368
384
  if (loadedSettings.processing) {
369
385
  merged.processing = { ...this.defaultConfig.processing, ...loadedSettings.processing };
370
386
  }
387
+
388
+ if (loadedSettings.autoTranslate) {
389
+ merged.autoTranslate = { ...this.defaultConfig.autoTranslate, ...loadedSettings.autoTranslate };
390
+ }
371
391
 
372
392
  if (loadedSettings.advanced) {
373
393
  merged.advanced = { ...this.defaultConfig.advanced, ...loadedSettings.advanced };
@@ -875,6 +895,95 @@ class SettingsManager {
875
895
  }
876
896
  }
877
897
  },
898
+ autoTranslate: {
899
+ type: 'object',
900
+ description: 'Auto Translate beta defaults',
901
+ properties: {
902
+ placeholderMode: {
903
+ type: 'string',
904
+ description: 'How placeholder-bearing strings are handled during auto translation',
905
+ enum: ['preserve', 'skip', 'send'],
906
+ default: 'preserve'
907
+ },
908
+ concurrency: {
909
+ type: 'number',
910
+ description: 'Maximum concurrent translation requests',
911
+ minimum: 1,
912
+ maximum: 25,
913
+ default: 6
914
+ },
915
+ batchSize: {
916
+ type: 'number',
917
+ description: 'Number of text segments scheduled per translation batch',
918
+ minimum: 1,
919
+ maximum: 10000,
920
+ default: 100
921
+ },
922
+ progressInterval: {
923
+ type: 'number',
924
+ description: 'Number of completed text segments between progress updates',
925
+ minimum: 1,
926
+ maximum: 10000,
927
+ default: 25
928
+ },
929
+ retryCount: {
930
+ type: 'number',
931
+ description: 'Maximum retries per failed translation request',
932
+ minimum: 0,
933
+ maximum: 10,
934
+ default: 3
935
+ },
936
+ retryDelay: {
937
+ type: 'number',
938
+ description: 'Base retry delay in milliseconds',
939
+ minimum: 0,
940
+ maximum: 30000,
941
+ default: 1000
942
+ },
943
+ timeout: {
944
+ type: 'number',
945
+ description: 'HTTP request timeout in milliseconds',
946
+ minimum: 1000,
947
+ maximum: 120000,
948
+ default: 15000
949
+ },
950
+ dryRunFirst: {
951
+ type: 'boolean',
952
+ description: 'Run a dry-run preview before interactive manager translation',
953
+ default: true
954
+ },
955
+ reportStdout: {
956
+ type: 'boolean',
957
+ description: 'Print the post-translation report to stdout',
958
+ default: true
959
+ },
960
+ bom: {
961
+ type: 'boolean',
962
+ description: 'Write translated JSON files with a UTF-8 BOM',
963
+ default: false
964
+ },
965
+ protectionEnabled: {
966
+ type: 'boolean',
967
+ description: 'Enable user-editable protected terms, keys, values, and patterns',
968
+ default: true
969
+ },
970
+ protectionFile: {
971
+ type: 'string',
972
+ description: 'Project JSON file containing Auto Translate protection rules',
973
+ default: './i18ntk-auto-translate.json'
974
+ },
975
+ promptProtectionSetup: {
976
+ type: 'boolean',
977
+ description: 'Ask to create the protection file when Auto Translate first runs',
978
+ default: true
979
+ },
980
+ promptProtectionUpdate: {
981
+ type: 'boolean',
982
+ description: 'Ask whether to update protection rules before manager translations',
983
+ default: true
984
+ }
985
+ }
986
+ },
878
987
  security: {
879
988
  type: 'object',
880
989
  properties: {
@@ -1003,4 +1112,3 @@ class SettingsManager {
1003
1112
  }
1004
1113
 
1005
1114
  module.exports = SettingsManager;
1006
-
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
- "version": "1.10.0",
4
- "updated": "2025-08-16"
3
+ "version": "3.1.0",
4
+ "updated": "2026-05-07"
5
5
  },
6
6
  "common": {
7
7
  "welcome": "Willkommen",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
- "version": "1.10.3",
4
- "updated": "2025-08-24"
3
+ "version": "3.1.0",
4
+ "updated": "2026-05-07"
5
5
  },
6
6
  "common": {
7
7
  "welcome": "Welcome",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
- "version": "1.10.0",
4
- "updated": "2025-08-16"
3
+ "version": "3.1.0",
4
+ "updated": "2026-05-07"
5
5
  },
6
6
  "common": {
7
7
  "welcome": "Bienvenido",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
- "version": "1.10.0",
4
- "updated": "2025-08-16"
3
+ "version": "3.1.0",
4
+ "updated": "2026-05-07"
5
5
  },
6
6
  "common": {
7
7
  "welcome": "Bienvenue",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
- "version": "1.10.0",
4
- "updated": "2025-08-16"
3
+ "version": "3.1.0",
4
+ "updated": "2026-05-07"
5
5
  },
6
6
  "common": {
7
7
  "welcome": "ようこそ",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
- "version": "1.10.0",
4
- "updated": "2025-08-16"
3
+ "version": "3.1.0",
4
+ "updated": "2026-05-07"
5
5
  },
6
6
  "common": {
7
7
  "welcome": "Добро пожаловать",
@@ -984,7 +984,8 @@
984
984
  "translation_not_found": "Перевод для ключа не найден: {key}",
985
985
  "translation_key_not_string": "Значение ключа перевода не является строкой: {key}",
986
986
  "error_reading_locales_directory": "Ошибка чтения директории локалей: {errorMessage}"
987
- }
987
+ },
988
+ "translateCommand": "translate - Автоперевод файлов локалей (Бета)"
988
989
  },
989
990
  "init": {
990
991
  "initializationTitle": "🚀 Набор инструментов i18n — Инициализация проекта",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
- "version": "1.10.0",
4
- "updated": "2025-08-16"
3
+ "version": "3.1.0",
4
+ "updated": "2026-05-07"
5
5
  },
6
6
  "common": {
7
7
  "welcome": "欢迎",
@@ -121,7 +121,7 @@ const DEFAULT_CONFIG = {
121
121
  "complete": null,
122
122
  "manage": null
123
123
  },
124
- "processing": {
124
+ "processing": {
125
125
  "batchSize": 2000,
126
126
  "concurrency": 32,
127
127
  "maxFileSize": 524288,
@@ -150,9 +150,25 @@ const DEFAULT_CONFIG = {
150
150
  "streaming": true,
151
151
  "compression": "brotli",
152
152
  "parallelProcessing": true,
153
- "minimalLogging": true
154
- },
155
- "reports": {
153
+ "minimalLogging": true
154
+ },
155
+ "autoTranslate": {
156
+ "placeholderMode": "preserve",
157
+ "concurrency": 6,
158
+ "batchSize": 100,
159
+ "progressInterval": 25,
160
+ "retryCount": 3,
161
+ "retryDelay": 1000,
162
+ "timeout": 15000,
163
+ "dryRunFirst": true,
164
+ "reportStdout": true,
165
+ "bom": false,
166
+ "protectionEnabled": true,
167
+ "protectionFile": "./i18ntk-auto-translate.json",
168
+ "promptProtectionSetup": true,
169
+ "promptProtectionUpdate": true
170
+ },
171
+ "reports": {
156
172
  "format": "json",
157
173
  "includeStats": true,
158
174
  "includeMissingKeys": true,
package/utils/security.js CHANGED
@@ -687,12 +687,13 @@ static _logging = false;
687
687
  'projectRoot', 'sourceDir', 'i18nDir', 'outputDir', 'backupDir', 'tempDir', 'cacheDir', 'configDir',
688
688
  // Language settings
689
689
  'sourceLanguage', 'uiLanguage', 'language', 'defaultLanguages', 'supportedLanguages',
690
- // Translation markers and content
691
- 'notTranslatedMarker', 'notTranslatedMarkers', 'translatedMarker', 'translatedMarkers',
690
+ // Translation markers and content
691
+ 'notTranslatedMarker', 'notTranslatedMarkers', 'translatedMarker', 'translatedMarkers',
692
+ 'allowedEnglishTerms', 'englishContentThresholdPercent',
692
693
  // File handling
693
694
  'supportedExtensions', 'excludeFiles', 'excludeDirs', 'includeFiles', 'includeDirs',
694
695
  // Operational settings
695
- 'strictMode', 'debug', 'displayPaths', 'version', 'scriptDirectories',
696
+ 'strictMode', 'debug', 'displayPaths', 'version', 'scriptDirectories', 'autoTranslate',
696
697
  // Framework and processing
697
698
  'framework', 'processing', 'performance', 'advanced',
698
699
  // UI and theme settings
@@ -6,18 +6,19 @@ const PLACEHOLDER_WARNING = [
6
6
  ' WARNING: DYNAMIC PLACEHOLDER TOKENS DETECTED',
7
7
  '============================================================',
8
8
  '',
9
- ' Google Translate will attempt to translate the ENTIRE',
10
- ' string value, including any placeholder tokens like:',
9
+ ' Auto Translate can preserve placeholders while translating',
10
+ ' only the text around tokens like:',
11
11
  '',
12
12
  ' {name} {{count}} %d %s :param ${var}',
13
13
  '',
14
- ' This WILL corrupt or alter your placeholders, which',
15
- ' will break runtime substitution in your application.',
14
+ ' Sending placeholders to a translation provider can corrupt',
15
+ ' runtime substitution in your application.',
16
16
  '',
17
- ' You have two choices for strings containing placeholders:',
17
+ ' You have three choices for strings containing placeholders:',
18
18
  '',
19
- ' SKIP - Copy verbatim (safe); manually translate later',
20
- ' SEND - Translate anyway (risky); may corrupt placeholders',
19
+ ' PRESERVE - Translate text segments and reinsert placeholders',
20
+ ' SKIP - Copy verbatim; manually translate later',
21
+ ' SEND - Translate anyway with masking',
21
22
  '',
22
23
  '============================================================',
23
24
  ].join('\n');
@@ -28,18 +29,20 @@ async function confirmGlobalChoice() {
28
29
  console.log(' What should we do with ALL strings that contain');
29
30
  console.log(' dynamic placeholder tokens?');
30
31
  console.log('');
31
- console.log(' [s] SKIP all - Copy verbatim, translate nothing with placeholders');
32
- console.log(' [t] SEND all - Translate everything, accept corruption risk');
33
- console.log(' [i] ASK each - Decide individually for each key');
32
+ console.log(' [p] PRESERVE all - Translate text around placeholders (recommended)');
33
+ console.log(' [s] SKIP all - Copy verbatim, translate nothing with placeholders');
34
+ console.log(' [t] SEND all - Translate everything with placeholder masking');
35
+ console.log(' [i] ASK each - Decide individually for each key');
34
36
  console.log('');
35
37
 
36
38
  while (true) {
37
- const answer = await ask(' Choice [s/t/i]: ');
39
+ const answer = await ask(' Choice [p/s/t/i]: ');
38
40
  const lower = answer.toLowerCase().trim();
41
+ if (lower === '' || lower === 'p' || lower === 'preserve') return { strategy: 'preserve', interactive: false };
39
42
  if (lower === 's' || lower === 'skip') return { strategy: 'skip', interactive: false };
40
43
  if (lower === 't' || lower === 'send') return { strategy: 'send', interactive: false };
41
- if (lower === 'i' || lower === 'ask' || lower === 'interactive') return { strategy: 'skip', interactive: true };
42
- console.log(' Please enter s, t, or i.');
44
+ if (lower === 'i' || lower === 'ask' || lower === 'interactive') return { strategy: 'preserve', interactive: true };
45
+ console.log(' Please enter p, s, t, or i.');
43
46
  }
44
47
  }
45
48
 
@@ -52,13 +55,14 @@ async function confirmPerKey(keyPath, value, placeholders) {
52
55
  console.log('');
53
56
 
54
57
  while (true) {
55
- const answer = await ask(' [s]kip / [t]ranslate anyway / s[k]ip all remaining / [a]ll remaining? ');
58
+ const answer = await ask(' [p]reserve / [s]kip / [t]ranslate masked / s[k]ip all / [a]ll preserve? ');
56
59
  const lower = answer.toLowerCase().trim();
60
+ if (lower === '' || lower === 'p' || lower === 'preserve') return 'preserve';
57
61
  if (lower === 's' || lower === 'skip') return 'skip';
58
62
  if (lower === 't' || lower === 'translate') return 'send';
59
63
  if (lower === 'k' || lower === 'skipall') return 'skip-all';
60
- if (lower === 'a' || lower === 'all') return 'send-all';
61
- console.log(' Please enter s, t, k, or a.');
64
+ if (lower === 'a' || lower === 'all') return 'preserve-all';
65
+ console.log(' Please enter p, s, t, k, or a.');
62
66
  }
63
67
  }
64
68
 
@@ -40,6 +40,7 @@ function detectPlaceholders(value, customPatterns) {
40
40
  const patterns = compilePatterns(customPatterns);
41
41
  const found = new Set();
42
42
  for (const pattern of patterns) {
43
+ pattern.lastIndex = 0;
43
44
  const matches = value.match(pattern);
44
45
  if (matches) {
45
46
  for (const m of matches) found.add(m);
@@ -52,11 +53,68 @@ function hasPlaceholders(value, customPatterns) {
52
53
  if (!value || typeof value !== 'string') return false;
53
54
  const patterns = compilePatterns(customPatterns);
54
55
  for (const pattern of patterns) {
56
+ pattern.lastIndex = 0;
55
57
  if (pattern.test(value)) return true;
56
58
  }
57
59
  return false;
58
60
  }
59
61
 
62
+ function splitByPlaceholders(value, customPatterns) {
63
+ if (!value || typeof value !== 'string') {
64
+ return [{ type: 'text', value: value || '' }];
65
+ }
66
+
67
+ const patterns = compilePatterns(customPatterns);
68
+ const matches = [];
69
+
70
+ for (const pattern of patterns) {
71
+ pattern.lastIndex = 0;
72
+ let match;
73
+ while ((match = pattern.exec(value)) !== null) {
74
+ if (!match[0]) {
75
+ pattern.lastIndex++;
76
+ continue;
77
+ }
78
+ matches.push({
79
+ start: match.index,
80
+ end: match.index + match[0].length,
81
+ value: match[0],
82
+ });
83
+ }
84
+ }
85
+
86
+ if (matches.length === 0) {
87
+ return [{ type: 'text', value }];
88
+ }
89
+
90
+ matches.sort((a, b) => {
91
+ if (a.start !== b.start) return a.start - b.start;
92
+ return (b.end - b.start) - (a.end - a.start);
93
+ });
94
+
95
+ const accepted = [];
96
+ for (const match of matches) {
97
+ const overlaps = accepted.some((item) => match.start < item.end && match.end > item.start);
98
+ if (!overlaps) accepted.push(match);
99
+ }
100
+ accepted.sort((a, b) => a.start - b.start);
101
+
102
+ const segments = [];
103
+ let cursor = 0;
104
+ for (const match of accepted) {
105
+ if (match.start > cursor) {
106
+ segments.push({ type: 'text', value: value.slice(cursor, match.start) });
107
+ }
108
+ segments.push({ type: 'placeholder', value: match.value });
109
+ cursor = match.end;
110
+ }
111
+ if (cursor < value.length) {
112
+ segments.push({ type: 'text', value: value.slice(cursor) });
113
+ }
114
+
115
+ return segments;
116
+ }
117
+
60
118
  function maskPlaceholders(value, customPatterns) {
61
119
  if (!value || typeof value !== 'string') return { masked: value, map: new Map() };
62
120
  const patterns = compilePatterns(customPatterns);
@@ -64,6 +122,7 @@ function maskPlaceholders(value, customPatterns) {
64
122
  let idx = 0;
65
123
  let masked = value;
66
124
  for (const pattern of patterns) {
125
+ pattern.lastIndex = 0;
67
126
  masked = masked.replace(pattern, (match) => {
68
127
  const ph = `\uE000${idx}\uE001`;
69
128
  map.set(ph, match);
@@ -88,6 +147,7 @@ module.exports = {
88
147
  compilePatterns,
89
148
  detectPlaceholders,
90
149
  hasPlaceholders,
150
+ splitByPlaceholders,
91
151
  maskPlaceholders,
92
152
  unmaskPlaceholders,
93
153
  };