i18ntk 1.0.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/LICENSE +21 -0
  3. package/README.md +507 -0
  4. package/dev/README.md +37 -0
  5. package/dev/debug/README.md +30 -0
  6. package/dev/debug/complete-console-translations.js +295 -0
  7. package/dev/debug/console-key-checker.js +408 -0
  8. package/dev/debug/console-translations.js +335 -0
  9. package/dev/debug/debugger.js +408 -0
  10. package/dev/debug/export-missing-keys.js +432 -0
  11. package/dev/debug/final-normalize.js +236 -0
  12. package/dev/debug/find-extra-keys.js +68 -0
  13. package/dev/debug/normalize-locales.js +153 -0
  14. package/dev/debug/refactor-locales.js +240 -0
  15. package/dev/debug/reorder-locales.js +85 -0
  16. package/dev/debug/replace-hardcoded-console.js +378 -0
  17. package/docs/INSTALLATION.md +449 -0
  18. package/docs/README.md +222 -0
  19. package/docs/TODO_ROADMAP.md +279 -0
  20. package/docs/api/API_REFERENCE.md +377 -0
  21. package/docs/api/COMPONENTS.md +492 -0
  22. package/docs/api/CONFIGURATION.md +651 -0
  23. package/docs/api/NPM_PUBLISHING_GUIDE.md +434 -0
  24. package/docs/debug/DEBUG_README.md +30 -0
  25. package/docs/debug/DEBUG_TOOLS.md +494 -0
  26. package/docs/development/AGENTS.md +351 -0
  27. package/docs/development/DEVELOPMENT_RULES.md +165 -0
  28. package/docs/development/DEV_README.md +37 -0
  29. package/docs/release-notes/RELEASE_NOTES_v1.0.0.md +173 -0
  30. package/docs/release-notes/RELEASE_NOTES_v1.6.0.md +141 -0
  31. package/docs/release-notes/RELEASE_NOTES_v1.6.1.md +185 -0
  32. package/docs/release-notes/RELEASE_NOTES_v1.6.3.md +199 -0
  33. package/docs/reports/ANALYSIS_README.md +17 -0
  34. package/docs/reports/CONSOLE_MISMATCH_BUG_REPORT_v1.5.0.md +181 -0
  35. package/docs/reports/SIZING_README.md +18 -0
  36. package/docs/reports/SUMMARY_README.md +18 -0
  37. package/docs/reports/TRANSLATION_BUG_REPORT_v1.5.0.md +129 -0
  38. package/docs/reports/USAGE_README.md +18 -0
  39. package/docs/reports/VALIDATION_README.md +18 -0
  40. package/locales/de/auth.json +3 -0
  41. package/locales/de/common.json +16 -0
  42. package/locales/de/pagination.json +6 -0
  43. package/locales/en/auth.json +3 -0
  44. package/locales/en/common.json +16 -0
  45. package/locales/en/pagination.json +6 -0
  46. package/locales/es/auth.json +3 -0
  47. package/locales/es/common.json +16 -0
  48. package/locales/es/pagination.json +6 -0
  49. package/locales/fr/auth.json +3 -0
  50. package/locales/fr/common.json +16 -0
  51. package/locales/fr/pagination.json +6 -0
  52. package/locales/ru/auth.json +3 -0
  53. package/locales/ru/common.json +16 -0
  54. package/locales/ru/pagination.json +6 -0
  55. package/main/i18ntk-analyze.js +625 -0
  56. package/main/i18ntk-autorun.js +461 -0
  57. package/main/i18ntk-complete.js +494 -0
  58. package/main/i18ntk-init.js +686 -0
  59. package/main/i18ntk-manage.js +848 -0
  60. package/main/i18ntk-sizing.js +557 -0
  61. package/main/i18ntk-summary.js +671 -0
  62. package/main/i18ntk-usage.js +1282 -0
  63. package/main/i18ntk-validate.js +762 -0
  64. package/main/ui-i18n.js +332 -0
  65. package/package.json +152 -0
  66. package/scripts/fix-missing-translation-keys.js +214 -0
  67. package/scripts/verify-package.js +168 -0
  68. package/ui-locales/de.json +637 -0
  69. package/ui-locales/en.json +688 -0
  70. package/ui-locales/es.json +637 -0
  71. package/ui-locales/fr.json +637 -0
  72. package/ui-locales/ja.json +637 -0
  73. package/ui-locales/ru.json +637 -0
  74. package/ui-locales/zh.json +637 -0
  75. package/utils/admin-auth.js +317 -0
  76. package/utils/admin-cli.js +353 -0
  77. package/utils/admin-pin.js +409 -0
  78. package/utils/detect-language-mismatches.js +454 -0
  79. package/utils/i18n-helper.js +128 -0
  80. package/utils/maintain-language-purity.js +433 -0
  81. package/utils/native-translations.js +478 -0
  82. package/utils/security.js +384 -0
  83. package/utils/test-complete-system.js +356 -0
  84. package/utils/test-console-i18n.js +402 -0
  85. package/utils/translate-mismatches.js +571 -0
  86. package/utils/validate-language-purity.js +531 -0
@@ -0,0 +1,494 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * I18N TRANSLATION COMPLETION SCRIPT
4
+ *
5
+ * This script automatically adds missing translation keys to achieve 100% coverage.
6
+ * It reads the usage analysis and adds all missing keys with proper markers.
7
+ *
8
+ * Usage:
9
+ * node i18ntk-complete.js
10
+ * node i18ntk-complete.js --auto-translate
11
+ * node i18ntk-complete.js --source-dir=./src/i18n/locales
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { execSync } = require('child_process');
17
+ const SecurityUtils = require('../utils/security');
18
+ const settingsManager = require('../settings/settings-manager');
19
+
20
+ // Get configuration from settings manager securely
21
+ async function getConfig() {
22
+ try {
23
+ const settings = await settingsManager.getSettings();
24
+ const config = {
25
+ sourceDir: settings.directories?.sourceDir || './locales',
26
+ sourceLanguage: settings.directories?.sourceLanguage || 'en',
27
+ notTranslatedMarker: settings.directories?.notTranslatedMarker || 'NOT_TRANSLATED',
28
+ excludeFiles: settings.processing?.excludeFiles || ['.DS_Store', 'Thumbs.db']
29
+ };
30
+
31
+ // Basic validation for required fields
32
+ if (!config.sourceDir || !config.sourceLanguage) {
33
+ throw new Error('Configuration validation failed: missing required fields');
34
+ }
35
+
36
+ SecurityUtils.logSecurityEvent('Configuration loaded successfully', 'info');
37
+ return config;
38
+ } catch (error) {
39
+ SecurityUtils.logSecurityEvent('Configuration loading failed', 'error', { error: error.message });
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ // Common missing keys that should be added to all projects
45
+ const COMMON_MISSING_KEYS = {
46
+ // Offline/Network
47
+ 'offlineTitle': 'You are offline',
48
+ 'offlineMessage': 'Please check your internet connection',
49
+ 'tryReconnect': 'Try to reconnect',
50
+
51
+ // Common UI
52
+ 'common': 'Common',
53
+ 'logout': 'Logout',
54
+ 'login': 'Login',
55
+ 'amount': 'Amount',
56
+
57
+ // Pagination
58
+ 'pagination.showing': 'Showing',
59
+ 'pagination.of': 'of',
60
+ 'pagination.items': 'items',
61
+ 'pagination.rowsPerPage': 'Rows per page',
62
+
63
+ // Report Generator
64
+ 'reportGenerator:reportTypes.prospects': 'Prospects Report',
65
+ 'reportGenerator:reportTypes.activities': 'Activities Report',
66
+ 'reportGenerator:reportTypes.goals': 'Goals Report',
67
+ 'reportGenerator:reportTypes.team': 'Team Report',
68
+ 'reportGenerator:reportTypes.hotLeads': 'Hot Leads Report',
69
+
70
+ 'reportGenerator:timePeriods.allTime': 'All Time',
71
+ 'reportGenerator:timePeriods.last7days': 'Last 7 Days',
72
+ 'reportGenerator:timePeriods.last30days': 'Last 30 Days',
73
+ 'reportGenerator:timePeriods.thisYear': 'This Year',
74
+
75
+ 'reportGenerator:layouts.summary': 'Summary Layout',
76
+ 'reportGenerator:layouts.detailed': 'Detailed Layout',
77
+ 'reportGenerator:layouts.visual_charts': 'Visual Charts Layout',
78
+
79
+ // Common time periods
80
+ 'common:timePeriods.customRange': 'Custom Range',
81
+
82
+ // Auth
83
+ 'auth:supabaseNotConfigured': 'Supabase is not configured',
84
+
85
+ // Common values
86
+ 'common:unknownUser': 'Unknown User',
87
+ 'common:notSet': 'Not Set',
88
+
89
+ // Validation
90
+ 'validationStep.invalidEmailFormat': 'Invalid email format',
91
+
92
+ // Admin
93
+ 'announcement_id': 'Announcement ID',
94
+ 'last_sign_in_at': 'Last Sign In'
95
+ };
96
+
97
+ class I18nCompletionTool {
98
+ constructor(config = {}) {
99
+ this.config = config;
100
+ this.sourceDir = null;
101
+ this.sourceLanguageDir = null;
102
+
103
+ // Initialize UI i18n for console messages
104
+ const UIi18n = require('./ui-i18n');
105
+ this.ui = new UIi18n();
106
+ this.t = this.ui.t.bind(this.ui);
107
+ }
108
+
109
+ async initialize() {
110
+ const baseConfig = await getConfig();
111
+ this.config = { ...baseConfig, ...this.config };
112
+ this.sourceDir = path.resolve(this.config.sourceDir);
113
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
114
+ }
115
+
116
+ // Parse command line arguments
117
+ parseArgs() {
118
+ const args = process.argv.slice(2);
119
+ const parsed = {};
120
+
121
+ args.forEach(arg => {
122
+ if (arg.startsWith('--')) {
123
+ const [key, value] = arg.substring(2).split('=');
124
+ if (key === 'source-dir') {
125
+ parsed.sourceDir = value;
126
+ } else if (key === 'auto-translate') {
127
+ parsed.autoTranslate = true;
128
+ } else if (key === 'dry-run') {
129
+ parsed.dryRun = true;
130
+ } else if (key === 'no-prompt') {
131
+ parsed.noPrompt = true;
132
+ }
133
+ }
134
+ });
135
+
136
+ return parsed;
137
+ }
138
+
139
+ // Get all available languages
140
+ getAvailableLanguages() {
141
+ if (!fs.existsSync(this.sourceDir)) {
142
+ throw new Error(`Source directory not found: ${this.sourceDir}`);
143
+ }
144
+
145
+ return fs.readdirSync(this.sourceDir)
146
+ .filter(item => {
147
+ const itemPath = path.join(this.sourceDir, item);
148
+ return fs.statSync(itemPath).isDirectory();
149
+ });
150
+ }
151
+
152
+ // Get all JSON files from a language directory
153
+ getLanguageFiles(language) {
154
+ const languageDir = path.join(this.sourceDir, language);
155
+
156
+ if (!fs.existsSync(languageDir)) {
157
+ return [];
158
+ }
159
+
160
+ return fs.readdirSync(languageDir)
161
+ .filter(file => {
162
+ return file.endsWith('.json') &&
163
+ !this.config.excludeFiles.includes(file);
164
+ });
165
+ }
166
+
167
+ // Parse key path and determine which file it belongs to
168
+ parseKeyPath(keyPath) {
169
+ // Handle namespace:key format (e.g., "reportGenerator:reportTypes.prospects")
170
+ if (keyPath.includes(':')) {
171
+ const [namespace, key] = keyPath.split(':', 2);
172
+ return {
173
+ file: `${namespace}.json`,
174
+ key: key
175
+ };
176
+ }
177
+
178
+ // Handle dot notation (e.g., "pagination.showing")
179
+ const parts = keyPath.split('.');
180
+ if (parts.length > 1) {
181
+ return {
182
+ file: `${parts[0]}.json`,
183
+ key: parts.slice(1).join('.')
184
+ };
185
+ }
186
+
187
+ // Default to common.json for simple keys
188
+ return {
189
+ file: 'common.json',
190
+ key: keyPath
191
+ };
192
+ }
193
+
194
+ // Check if nested key exists in object
195
+ hasNestedKey(obj, keyPath) {
196
+ const keys = keyPath.split('.');
197
+ let current = obj;
198
+
199
+ for (const key of keys) {
200
+ if (!current || typeof current !== 'object' || !(key in current)) {
201
+ return false;
202
+ }
203
+ current = current[key];
204
+ }
205
+
206
+ return true;
207
+ }
208
+
209
+ // Set nested value in object
210
+ setNestedValue(obj, keyPath, value) {
211
+ const keys = keyPath.split('.');
212
+ let current = obj;
213
+
214
+ for (let i = 0; i < keys.length - 1; i++) {
215
+ const key = keys[i];
216
+ if (!(key in current) || typeof current[key] !== 'object') {
217
+ current[key] = {};
218
+ }
219
+ current = current[key];
220
+ }
221
+
222
+ current[keys[keys.length - 1]] = value;
223
+ }
224
+
225
+ // Add missing keys to a language
226
+ addMissingKeysToLanguage(language, missingKeys, dryRun = false) {
227
+ const languageDir = path.join(this.sourceDir, language);
228
+ const changes = [];
229
+
230
+ // Group keys by file
231
+ const keysByFile = {};
232
+
233
+ missingKeys.forEach(keyPath => {
234
+ const { file, key } = this.parseKeyPath(keyPath);
235
+ if (!keysByFile[file]) {
236
+ keysByFile[file] = [];
237
+ }
238
+ keysByFile[file].push({ keyPath, key });
239
+ });
240
+
241
+ // Process each file
242
+ for (const [fileName, keys] of Object.entries(keysByFile)) {
243
+ const filePath = path.join(languageDir, fileName);
244
+ let fileContent = {};
245
+
246
+ // Load existing file or create new
247
+ if (fs.existsSync(filePath)) {
248
+ try {
249
+ fileContent = JSON.parse(fs.readFileSync(filePath, 'utf8'));
250
+ } catch (error) {
251
+ console.warn(this.t("completeTranslations.warning_could_not_parse_filepa", { filePath })); ;
252
+ fileContent = {};
253
+ }
254
+ } else {
255
+ // Create directory if it doesn't exist
256
+ if (!fs.existsSync(languageDir)) {
257
+ if (!dryRun) {
258
+ fs.mkdirSync(languageDir, { recursive: true });
259
+ }
260
+ }
261
+ }
262
+
263
+ // Add missing keys
264
+ let fileChanged = false;
265
+ keys.forEach(({ keyPath, key }) => {
266
+ // Check if key already exists
267
+ if (!this.hasNestedKey(fileContent, key)) {
268
+ const value = this.generateTranslationValue(keyPath, language);
269
+
270
+ this.setNestedValue(fileContent, key, value);
271
+ fileChanged = true;
272
+
273
+ changes.push({
274
+ file: fileName,
275
+ key: keyPath,
276
+ value,
277
+ action: 'added'
278
+ });
279
+ }
280
+ });
281
+
282
+ // Save file
283
+ if (fileChanged && !dryRun) {
284
+ fs.writeFileSync(filePath, JSON.stringify(fileContent, null, 2), 'utf8');
285
+ }
286
+ }
287
+
288
+ return changes;
289
+ }
290
+
291
+ // Generate appropriate translation value based on key and language
292
+ generateTranslationValue(keyPath, language) {
293
+ // Use English as base value from COMMON_MISSING_KEYS or generate from key
294
+ const baseValue = COMMON_MISSING_KEYS[keyPath] || this.generateValueFromKey(keyPath);
295
+
296
+ // For source language, use the base value (never use namespace prefix)
297
+ if (language === this.config.sourceLanguage) {
298
+ return baseValue;
299
+ }
300
+
301
+ // For other languages, use the not translated marker
302
+ return 'NOT_TRANSLATED';
303
+ }
304
+
305
+ // Generate a readable value from a key path
306
+ generateValueFromKey(keyPath) {
307
+ // Extract the last part of the key (after dots and colons)
308
+ const keyName = keyPath.split('.').pop().split(':').pop();
309
+
310
+ // Convert camelCase to readable text
311
+ const readable = keyName
312
+ .replace(/([A-Z])/g, ' $1')
313
+ .replace(/^./, str => str.toUpperCase())
314
+ .trim();
315
+
316
+ // Never return the namespace prefix as the value
317
+ return readable || keyName;
318
+ }
319
+
320
+ // Get missing keys from usage analysis
321
+ getMissingKeysFromUsage() {
322
+ const usageReportPath = path.join(process.cwd(), 'scripts', 'i18n', 'reports', 'usage-analysis.txt');
323
+
324
+ // Delete old report to ensure fresh data
325
+ if (fs.existsSync(usageReportPath)) {
326
+ console.log(this.t("operations.complete.deletingOldReport"));
327
+ fs.unlinkSync(usageReportPath);
328
+ }
329
+
330
+ // Generate fresh usage analysis report
331
+ console.log(this.t("operations.complete.generatingFreshAnalysis"));
332
+ const { execSync } = require('child_process');
333
+ try {
334
+ execSync('node main/i18ntk-usage.js --output-report', {
335
+ stdio: 'inherit',
336
+ cwd: process.cwd()
337
+ });
338
+ } catch (error) {
339
+ console.log(this.t("operations.complete.couldNotGenerate"));
340
+ return Object.keys(COMMON_MISSING_KEYS);
341
+ }
342
+
343
+ if (!fs.existsSync(usageReportPath)) {
344
+ console.log(this.t("operations.complete.reportNotFound"));
345
+ return Object.keys(COMMON_MISSING_KEYS);
346
+ }
347
+
348
+ try {
349
+ const reportContent = fs.readFileSync(usageReportPath, 'utf8');
350
+ const missingKeys = [];
351
+
352
+ // Extract missing keys from the report
353
+ const lines = reportContent.split('\n');
354
+ let inMissingSection = false;
355
+
356
+ for (const line of lines) {
357
+ if (line.includes('MISSING TRANSLATION KEYS')) {
358
+ inMissingSection = true;
359
+ continue;
360
+ }
361
+
362
+ if (inMissingSection) {
363
+ // Stop when we reach another section
364
+ if (line.includes('DYNAMIC KEYS') || line.includes('USED KEYS') || line.includes('UNUSED KEYS')) {
365
+ break;
366
+ }
367
+
368
+ // Extract key from lines like "⚠️ offlineTitle"
369
+ const match = line.match(/^⚠️\s+(.+)$/);
370
+ if (match) {
371
+ const key = match[1].trim();
372
+ // Skip lines that are file paths or other metadata
373
+ if (!key.includes('Used in:') && !key.includes('📄') && key.length > 0 && !missingKeys.includes(key)) {
374
+ missingKeys.push(key);
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ console.log(this.t("operations.complete.foundMissingKeys", { count: missingKeys.length }));
381
+ return missingKeys.length > 0 ? missingKeys : Object.keys(COMMON_MISSING_KEYS);
382
+ } catch (error) {
383
+ console.log(this.t("operations.complete.couldNotParse"));
384
+ return Object.keys(COMMON_MISSING_KEYS);
385
+ }
386
+ }
387
+
388
+ // Run the completion process
389
+ async run() {
390
+ SecurityUtils.logSecurityEvent('I18n completion tool started', 'info', {
391
+ version: '1.3.7',
392
+ nodeVersion: process.version,
393
+ platform: process.platform
394
+ });
395
+
396
+ await this.initialize();
397
+
398
+ const args = this.parseArgs();
399
+
400
+ if (args.sourceDir) {
401
+ this.config.sourceDir = args.sourceDir;
402
+ this.sourceDir = path.resolve(this.config.sourceDir);
403
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
404
+ }
405
+
406
+ console.log(this.t("operations.complete.title"));
407
+ console.log(this.t("operations.complete.separator"));
408
+ console.log(this.t("operations.complete.sourceDir", { sourceDir: this.sourceDir }));
409
+ console.log(this.t("operations.complete.sourceLanguage", { sourceLanguage: this.config.sourceLanguage }));
410
+
411
+ if (args.dryRun) {
412
+ console.log(this.t("operations.complete.dryRunMode"));
413
+ }
414
+
415
+ try {
416
+ // Get available languages
417
+ const languages = this.getAvailableLanguages();
418
+ console.log(this.t("operations.complete.languages", { languages: languages.join(', ') }));
419
+
420
+ // Get missing keys from usage analysis or use common keys
421
+ const missingKeys = this.getMissingKeysFromUsage();
422
+ console.log(this.t("operations.complete.addingMissingKeys"));
423
+
424
+ let totalChanges = 0;
425
+
426
+ // Process each language
427
+ for (const language of languages) {
428
+ console.log(this.t("operations.complete.processing", { language }));
429
+
430
+ const changes = this.addMissingKeysToLanguage(language, missingKeys, args.dryRun);
431
+
432
+ if (changes.length > 0) {
433
+ console.log(this.t("operations.complete.addedKeys", { count: changes.length }));
434
+ totalChanges += changes.length;
435
+
436
+ // Show sample of changes
437
+ const sampleChanges = changes.slice(0, 3);
438
+ sampleChanges.forEach(change => {
439
+ console.log(this.t("operations.complete.changeDetails", { file: change.file, key: change.key }));
440
+ });
441
+
442
+ if (changes.length > 3) {
443
+ console.log(this.t("complete.andMore", { count: changes.length - 3 }));
444
+ }
445
+ } else {
446
+ console.log(this.t("operations.complete.noChangesNeeded", { language }));
447
+ }
448
+ }
449
+
450
+ console.log('\n');
451
+ console.log(this.t("operations.complete.summaryTitle"));
452
+ console.log(this.t("operations.complete.separator"));
453
+ console.log(this.t("operations.complete.totalChanges", { totalChanges }));
454
+ console.log(this.t("operations.complete.languagesProcessed", { languagesProcessed: languages.length }));
455
+ console.log(this.t("operations.complete.missingKeysAdded", { missingKeysAdded: missingKeys.length }));
456
+
457
+ if (!args.dryRun) {
458
+ console.log('\n' + this.t("operations.complete.nextStepsTitle"));
459
+ console.log(this.t("operations.complete.separator"));
460
+ console.log(this.t("operations.complete.nextStep1"));
461
+ console.log(' node i18ntk-usage.js --output-report');
462
+ console.log(this.t("complete.nextStep2"));
463
+ console.log(' node i18ntk-validate.js');
464
+ console.log(this.t("complete.nextStep3"));
465
+ console.log(' node i18ntk-analyze.js');
466
+ console.log('\n' + this.t("operations.complete.allKeysAvailable"));
467
+ } else {
468
+ console.log('\n' + this.t("operations.complete.runWithoutDryRun"));
469
+ }
470
+
471
+ // Only prompt when run from the menu (i.e., when a callback or menu context is present)
472
+ if (typeof this.prompt === "function" && args.fromMenu) {
473
+ console.log(this.ui.t('operations.completed'));
474
+ await this.prompt(this.ui.t('hardcodedTexts.pressEnterToContinue'));
475
+ }
476
+
477
+ } catch (error) {
478
+ console.error('Error during completion:', error.message);
479
+ process.exit(1);
480
+ }
481
+ }
482
+ }
483
+
484
+ // Run if called directly
485
+ if (require.main === module) {
486
+ const tool = new I18nCompletionTool();
487
+ tool.run().catch(error => {
488
+ console.error('Fatal error:', error.message);
489
+ SecurityUtils.logSecurityEvent('I18n completion tool failed', 'error', { error: error.message });
490
+ process.exit(1);
491
+ });
492
+ }
493
+
494
+ module.exports = I18nCompletionTool;