@pie-players/pie-players-shared 0.3.29 → 0.3.30

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 (142) hide show
  1. package/{src → dist}/components/PieItemPlayer.svelte +156 -88
  2. package/dist/i18n/translations/en/tools.json +1 -1
  3. package/{src → dist}/i18n/use-i18n-standalone.svelte.ts +1 -0
  4. package/{src → dist}/i18n/use-i18n.svelte.ts +1 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/loader-config.d.ts +25 -0
  10. package/dist/loader-config.d.ts.map +1 -1
  11. package/dist/loader-config.js +5 -0
  12. package/dist/loader-config.js.map +1 -1
  13. package/dist/pie/config.d.ts.map +1 -1
  14. package/dist/pie/config.js +12 -5
  15. package/dist/pie/config.js.map +1 -1
  16. package/dist/pie/iife-loader.d.ts +35 -0
  17. package/dist/pie/iife-loader.d.ts.map +1 -1
  18. package/dist/pie/iife-loader.js +138 -2
  19. package/dist/pie/iife-loader.js.map +1 -1
  20. package/dist/pie/index.d.ts +3 -1
  21. package/dist/pie/index.d.ts.map +1 -1
  22. package/dist/pie/index.js +1 -0
  23. package/dist/pie/index.js.map +1 -1
  24. package/dist/pie/overrides.d.ts +9 -0
  25. package/dist/pie/overrides.d.ts.map +1 -1
  26. package/dist/pie/overrides.js +36 -0
  27. package/dist/pie/overrides.js.map +1 -1
  28. package/dist/pie/resource-monitor.d.ts +17 -0
  29. package/dist/pie/resource-monitor.d.ts.map +1 -1
  30. package/dist/pie/resource-monitor.js +284 -81
  31. package/dist/pie/resource-monitor.js.map +1 -1
  32. package/dist/pie/updates.d.ts.map +1 -1
  33. package/dist/pie/updates.js +65 -2
  34. package/dist/pie/updates.js.map +1 -1
  35. package/dist/security/index.d.ts +5 -0
  36. package/dist/security/index.d.ts.map +1 -0
  37. package/dist/security/index.js +5 -0
  38. package/dist/security/index.js.map +1 -0
  39. package/dist/security/sanitize-item-markup.d.ts +46 -0
  40. package/dist/security/sanitize-item-markup.d.ts.map +1 -0
  41. package/dist/security/sanitize-item-markup.js +174 -0
  42. package/dist/security/sanitize-item-markup.js.map +1 -0
  43. package/dist/security/sanitize-svg-icon.d.ts +15 -0
  44. package/dist/security/sanitize-svg-icon.d.ts.map +1 -0
  45. package/dist/security/sanitize-svg-icon.js +89 -0
  46. package/dist/security/sanitize-svg-icon.js.map +1 -0
  47. package/dist/security/validate-style-url.d.ts +28 -0
  48. package/dist/security/validate-style-url.d.ts.map +1 -0
  49. package/dist/security/validate-style-url.js +58 -0
  50. package/dist/security/validate-style-url.js.map +1 -0
  51. package/dist/security/wrap-overwide-images.d.ts +31 -0
  52. package/dist/security/wrap-overwide-images.d.ts.map +1 -0
  53. package/dist/security/wrap-overwide-images.js +92 -0
  54. package/dist/security/wrap-overwide-images.js.map +1 -0
  55. package/dist/server/npm-registry.d.ts +8 -0
  56. package/dist/server/npm-registry.d.ts.map +1 -0
  57. package/dist/server/npm-registry.js +60 -0
  58. package/dist/server/npm-registry.js.map +1 -0
  59. package/dist/types/index.d.ts +10 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/types/index.js.map +1 -1
  62. package/dist/ui/first-focusable.d.ts +21 -0
  63. package/dist/ui/first-focusable.d.ts.map +1 -0
  64. package/dist/ui/first-focusable.js +73 -0
  65. package/dist/ui/first-focusable.js.map +1 -0
  66. package/dist/ui/focus-trap.d.ts.map +1 -1
  67. package/dist/ui/focus-trap.js +2 -13
  68. package/dist/ui/focus-trap.js.map +1 -1
  69. package/package.json +44 -34
  70. package/dist/i18n/scripts/check-coverage.d.ts +0 -16
  71. package/dist/i18n/scripts/check-coverage.d.ts.map +0 -1
  72. package/dist/i18n/scripts/check-coverage.js +0 -262
  73. package/dist/i18n/scripts/check-coverage.js.map +0 -1
  74. package/dist/i18n/scripts/scan-hardcoded.d.ts +0 -16
  75. package/dist/i18n/scripts/scan-hardcoded.d.ts.map +0 -1
  76. package/dist/i18n/scripts/scan-hardcoded.js +0 -266
  77. package/dist/i18n/scripts/scan-hardcoded.js.map +0 -1
  78. package/dist/i18n/use-i18n-standalone.svelte.d.ts +0 -87
  79. package/dist/i18n/use-i18n-standalone.svelte.d.ts.map +0 -1
  80. package/dist/i18n/use-i18n-standalone.svelte.js +0 -151
  81. package/dist/i18n/use-i18n-standalone.svelte.js.map +0 -1
  82. package/dist/i18n/use-i18n.svelte.d.ts +0 -67
  83. package/dist/i18n/use-i18n.svelte.d.ts.map +0 -1
  84. package/dist/i18n/use-i18n.svelte.js +0 -144
  85. package/dist/i18n/use-i18n.svelte.js.map +0 -1
  86. package/dist/instrumentation/providers/DataDogInstrumentationProvider.d.ts +0 -170
  87. package/dist/instrumentation/providers/DataDogInstrumentationProvider.d.ts.map +0 -1
  88. package/dist/instrumentation/providers/DataDogInstrumentationProvider.js +0 -183
  89. package/dist/instrumentation/providers/DataDogInstrumentationProvider.js.map +0 -1
  90. package/dist/theming/css-variables.d.ts +0 -7
  91. package/dist/theming/css-variables.d.ts.map +0 -1
  92. package/dist/theming/css-variables.js +0 -43
  93. package/dist/theming/css-variables.js.map +0 -1
  94. package/dist/theming/index.d.ts +0 -4
  95. package/dist/theming/index.d.ts.map +0 -1
  96. package/dist/theming/index.js +0 -3
  97. package/dist/theming/index.js.map +0 -1
  98. package/dist/theming/presets.d.ts +0 -7
  99. package/dist/theming/presets.d.ts.map +0 -1
  100. package/dist/theming/presets.js +0 -146
  101. package/dist/theming/presets.js.map +0 -1
  102. package/dist/theming/types.d.ts +0 -5
  103. package/dist/theming/types.d.ts.map +0 -1
  104. package/dist/theming/types.js +0 -2
  105. package/dist/theming/types.js.map +0 -1
  106. package/dist/types/custom-elements.d.ts +0 -158
  107. package/dist/types/custom-elements.d.ts.map +0 -1
  108. package/dist/types/custom-elements.js +0 -8
  109. package/dist/types/custom-elements.js.map +0 -1
  110. package/dist/types/search.d.ts +0 -105
  111. package/dist/types/search.d.ts.map +0 -1
  112. package/dist/types/search.js +0 -12
  113. package/dist/types/search.js.map +0 -1
  114. package/dist/types/transform.d.ts +0 -48
  115. package/dist/types/transform.d.ts.map +0 -1
  116. package/dist/types/transform.js +0 -21
  117. package/dist/types/transform.js.map +0 -1
  118. package/src/i18n/README.md +0 -223
  119. package/src/i18n/index.ts +0 -26
  120. package/src/i18n/loader.ts +0 -156
  121. package/src/i18n/scripts/check-coverage.ts +0 -345
  122. package/src/i18n/scripts/scan-hardcoded.ts +0 -342
  123. package/src/i18n/simple-i18n.ts +0 -236
  124. package/src/i18n/translations/ar/common.json +0 -36
  125. package/src/i18n/translations/ar/toolkit.json +0 -48
  126. package/src/i18n/translations/ar/tools.json +0 -103
  127. package/src/i18n/translations/en/common.json +0 -36
  128. package/src/i18n/translations/en/toolkit.json +0 -48
  129. package/src/i18n/translations/en/tools.json +0 -103
  130. package/src/i18n/translations/es/common.json +0 -36
  131. package/src/i18n/translations/es/toolkit.json +0 -48
  132. package/src/i18n/translations/es/tools.json +0 -103
  133. package/src/i18n/translations/zh/common.json +0 -36
  134. package/src/i18n/translations/zh/toolkit.json +0 -48
  135. package/src/i18n/translations/zh/tools.json +0 -103
  136. package/src/i18n/types.ts +0 -66
  137. /package/{src → dist}/components/PiePreviewLayout.svelte +0 -0
  138. /package/{src → dist}/components/PiePreviewToggle.svelte +0 -0
  139. /package/{src → dist}/components/PieSpinner.svelte +0 -0
  140. /package/{src → dist}/components/ToolSettingsButton.svelte +0 -0
  141. /package/{src → dist}/components/ToolSettingsPanel.svelte +0 -0
  142. /package/{src → dist}/components/index.ts +0 -0
@@ -1,345 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Translation Coverage Checker
4
- *
5
- * Validates that all locales have complete translations compared to the reference locale (English).
6
- * Adapted from pie-qti's translation coverage checker for JSON-based translations.
7
- *
8
- * Usage:
9
- * bun run packages/players-shared/src/i18n/scripts/check-coverage.ts
10
- *
11
- * Exit codes:
12
- * 0 - All translations complete
13
- * 1 - Missing translations found
14
- */
15
-
16
- import { readFileSync } from "fs";
17
- import { dirname, join, resolve } from "path";
18
- import { fileURLToPath } from "url";
19
-
20
- const __filename = fileURLToPath(import.meta.url);
21
- const __dirname = dirname(__filename);
22
-
23
- interface CoverageResult {
24
- locale: string;
25
- coverage: number; // Percentage
26
- totalKeys: number;
27
- translatedKeys: number;
28
- missing: string[];
29
- extra: string[];
30
- untranslated: string[];
31
- }
32
-
33
- /**
34
- * Extract all keys from a translation object recursively
35
- */
36
- function extractKeys(obj: any, prefix = ""): Set<string> {
37
- const keys = new Set<string>();
38
-
39
- for (const [key, value] of Object.entries(obj)) {
40
- const fullKey = prefix ? `${prefix}.${key}` : key;
41
-
42
- if (typeof value === "object" && value !== null) {
43
- // Check if it's a plural form (has 'one' and 'other' keys)
44
- if ("one" in value && "other" in value) {
45
- keys.add(fullKey);
46
- } else {
47
- // Recurse for nested objects
48
- const nested = extractKeys(value, fullKey);
49
- nested.forEach((k) => keys.add(k));
50
- }
51
- } else {
52
- keys.add(fullKey);
53
- }
54
- }
55
-
56
- return keys;
57
- }
58
-
59
- /**
60
- * Load all translations for a locale
61
- */
62
- function loadLocaleTranslations(locale: string): Record<string, any> {
63
- const basePath = resolve(__dirname, "../translations", locale);
64
-
65
- try {
66
- const common = JSON.parse(
67
- readFileSync(join(basePath, "common.json"), "utf-8"),
68
- );
69
- const toolkit = JSON.parse(
70
- readFileSync(join(basePath, "toolkit.json"), "utf-8"),
71
- );
72
- const tools = JSON.parse(
73
- readFileSync(join(basePath, "tools.json"), "utf-8"),
74
- );
75
-
76
- return { ...common, ...toolkit, ...tools };
77
- } catch (error) {
78
- console.error(`Error loading translations for locale ${locale}:`, error);
79
- throw error;
80
- }
81
- }
82
-
83
- /**
84
- * Get value at a dot-notation path
85
- */
86
- function getValueAtPath(obj: any, path: string): any {
87
- const keys = path.split(".");
88
- let current = obj;
89
-
90
- for (const key of keys) {
91
- if (current && typeof current === "object" && key in current) {
92
- current = current[key];
93
- } else {
94
- return undefined;
95
- }
96
- }
97
-
98
- return current;
99
- }
100
-
101
- /**
102
- * Check if a value looks like English (starts with uppercase letter)
103
- */
104
- function looksLikeEnglish(value: any): boolean {
105
- if (typeof value !== "string") return false;
106
- return /^[A-Z]/.test(value) && value.length > 1;
107
- }
108
-
109
- /**
110
- * Check translation coverage for a locale
111
- */
112
- function checkLocale(
113
- referenceKeys: Set<string>,
114
- referenceTranslations: Record<string, any>,
115
- targetLocale: string,
116
- ): CoverageResult {
117
- const targetTranslations = loadLocaleTranslations(targetLocale);
118
- const targetKeys = extractKeys(targetTranslations);
119
-
120
- // Missing keys (in reference but not in target)
121
- const missing = Array.from(referenceKeys).filter(
122
- (key) => !targetKeys.has(key),
123
- );
124
-
125
- // Extra keys (in target but not in reference)
126
- const extra = Array.from(targetKeys).filter((key) => !referenceKeys.has(key));
127
-
128
- // Potentially untranslated (same value as reference and looks like English)
129
- const untranslated = Array.from(targetKeys).filter((key) => {
130
- const refValue = getValueAtPath(referenceTranslations, key);
131
- const targetValue = getValueAtPath(targetTranslations, key);
132
-
133
- // For plural forms, check both 'one' and 'other'
134
- if (typeof refValue === "object" && "one" in refValue) {
135
- return (
136
- refValue.one === targetValue.one &&
137
- refValue.other === targetValue.other &&
138
- looksLikeEnglish(targetValue.one)
139
- );
140
- }
141
-
142
- if (typeof refValue === "string" && typeof targetValue === "string") {
143
- return refValue === targetValue && looksLikeEnglish(targetValue);
144
- }
145
-
146
- return false;
147
- });
148
-
149
- const translatedKeys = referenceKeys.size - missing.length;
150
- const coverage =
151
- referenceKeys.size > 0 ? (translatedKeys / referenceKeys.size) * 100 : 0;
152
-
153
- return {
154
- locale: targetLocale,
155
- coverage,
156
- totalKeys: referenceKeys.size,
157
- translatedKeys,
158
- missing,
159
- extra,
160
- untranslated,
161
- };
162
- }
163
-
164
- /**
165
- * Format coverage report
166
- */
167
- function formatReport(results: CoverageResult[]): string {
168
- let report = "\n";
169
- report += "┌─────────────────────────────────────────────────────┐\n";
170
- report += "│ PIE Players Translation Coverage Report │\n";
171
- report += `│ Reference Locale: en (${results[0]?.totalKeys || 0} keys) │\n`;
172
- report += "└─────────────────────────────────────────────────────┘\n\n";
173
-
174
- for (const result of results) {
175
- report += `📋 Locale: ${result.locale}\n`;
176
- report += "─────────────────────────────────────────────────────\n";
177
-
178
- const coverageEmoji =
179
- result.coverage === 100 ? "✅" : result.coverage >= 95 ? "⚠️ " : "❌";
180
-
181
- report += `${coverageEmoji} Coverage: ${result.coverage.toFixed(1)}% `;
182
- report += `(${result.translatedKeys}/${result.totalKeys} keys)\n\n`;
183
-
184
- if (
185
- result.coverage === 100 &&
186
- result.extra.length === 0 &&
187
- result.untranslated.length === 0
188
- ) {
189
- report += "✅ All keys present and translated!\n\n";
190
- } else {
191
- if (result.missing.length > 0) {
192
- report += `❌ Missing Keys (${result.missing.length}):\n`;
193
- const displayCount = Math.min(result.missing.length, 15);
194
- result.missing.slice(0, displayCount).forEach((key) => {
195
- report += ` • ${key}\n`;
196
- });
197
- if (result.missing.length > displayCount) {
198
- report += ` ... and ${result.missing.length - displayCount} more\n`;
199
- }
200
- report += "\n";
201
- }
202
-
203
- if (result.extra.length > 0) {
204
- report += `⚠️ Extra Keys (${result.extra.length}) - Not in reference locale:\n`;
205
- const displayCount = Math.min(result.extra.length, 10);
206
- result.extra.slice(0, displayCount).forEach((key) => {
207
- report += ` • ${key}\n`;
208
- });
209
- if (result.extra.length > displayCount) {
210
- report += ` ... and ${result.extra.length - displayCount} more\n`;
211
- }
212
- report += "\n";
213
- }
214
-
215
- if (result.untranslated.length > 0) {
216
- report += `⚠️ Potentially Untranslated (${result.untranslated.length}):\n`;
217
- const displayCount = Math.min(result.untranslated.length, 10);
218
- result.untranslated.slice(0, displayCount).forEach((key) => {
219
- const value = getValueAtPath(
220
- loadLocaleTranslations(result.locale),
221
- key,
222
- );
223
- const displayValue =
224
- typeof value === "string" ? value : JSON.stringify(value);
225
- report += ` • ${key} = "${displayValue}"\n`;
226
- });
227
- if (result.untranslated.length > displayCount) {
228
- report += ` ... and ${result.untranslated.length - displayCount} more\n`;
229
- }
230
- report += "\n";
231
- }
232
- }
233
- }
234
-
235
- // Summary
236
- report += "════════════════════════════════════════════════════════\n";
237
- report += "📊 Summary:\n";
238
- report += "────────────────────────────────────────────────────────\n";
239
-
240
- for (const result of results) {
241
- const emoji =
242
- result.coverage === 100 ? "✅" : result.coverage >= 95 ? "⚠️ " : "❌";
243
- const status =
244
- result.coverage === 100
245
- ? result.extra.length > 0 || result.untranslated.length > 0
246
- ? `Complete (${result.extra.length} extra, ${result.untranslated.length} untranslated)`
247
- : "Complete"
248
- : `Missing: ${result.missing.length}`;
249
-
250
- report += ` ${result.locale}: ${result.coverage.toFixed(1)}% `;
251
- report += `(${result.translatedKeys}/${result.totalKeys}) `;
252
- report += `${emoji} ${status}\n`;
253
- }
254
-
255
- const avgCoverage =
256
- results.reduce((sum, r) => sum + r.coverage, 0) / results.length;
257
- report += `\nOverall: ${avgCoverage.toFixed(1)}% average coverage\n`;
258
-
259
- const allComplete = results.every((r) => r.coverage === 100);
260
- const hasIssues = results.some(
261
- (r) => r.extra.length > 0 || r.untranslated.length > 0,
262
- );
263
-
264
- if (allComplete && !hasIssues) {
265
- report += "✅ All translations complete!\n";
266
- } else if (allComplete) {
267
- report +=
268
- "⚠️ All keys present, but some issues found (extra keys or untranslated)\n";
269
- } else {
270
- report += "❌ Some translations incomplete\n";
271
- }
272
-
273
- report += "════════════════════════════════════════════════════════\n";
274
-
275
- return report;
276
- }
277
-
278
- /**
279
- * Main entry point
280
- */
281
- async function main() {
282
- console.log("🔍 Checking translation coverage...\n");
283
-
284
- try {
285
- // Load reference locale (English)
286
- const referenceTranslations = loadLocaleTranslations("en");
287
- const referenceKeys = extractKeys(referenceTranslations);
288
-
289
- console.log(`📖 Reference locale loaded: ${referenceKeys.size} keys\n`);
290
-
291
- // Check all other locales
292
- const targetLocales = ["es", "zh", "ar"];
293
- const results: CoverageResult[] = [];
294
-
295
- for (const locale of targetLocales) {
296
- try {
297
- const result = checkLocale(
298
- referenceKeys,
299
- referenceTranslations,
300
- locale,
301
- );
302
- results.push(result);
303
- } catch (error) {
304
- console.error(`❌ Error checking locale ${locale}:`, error);
305
- process.exit(1);
306
- }
307
- }
308
-
309
- // Print report
310
- const report = formatReport(results);
311
- console.log(report);
312
-
313
- // Exit with error if any locale is incomplete
314
- const allComplete = results.every((r) => r.coverage === 100);
315
- if (!allComplete) {
316
- console.error(
317
- "\n❌ Exiting with error code 1 (translations incomplete)\n",
318
- );
319
- process.exit(1);
320
- }
321
-
322
- // Warn if there are extra keys or untranslated strings
323
- const hasIssues = results.some(
324
- (r) => r.extra.length > 0 || r.untranslated.length > 0,
325
- );
326
- if (hasIssues) {
327
- console.warn(
328
- "\n⚠️ Warning: Some locales have extra keys or potentially untranslated strings.\n" +
329
- " Consider reviewing and cleaning up these issues.\n",
330
- );
331
- // Don't exit with error for warnings, just inform
332
- }
333
-
334
- console.log("✅ Translation coverage check passed!\n");
335
- process.exit(0);
336
- } catch (error) {
337
- console.error("\n❌ Fatal error during coverage check:", error);
338
- process.exit(1);
339
- }
340
- }
341
-
342
- // Run if executed directly
343
- if (import.meta.url === `file://${process.argv[1]}`) {
344
- main();
345
- }
@@ -1,342 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Hardcoded String Scanner
4
- *
5
- * Scans component files for hardcoded English strings that should use i18n.
6
- * Adapted from pie-qti's hardcoded string scanner.
7
- *
8
- * Usage:
9
- * bun run packages/players-shared/src/i18n/scripts/scan-hardcoded.ts
10
- *
11
- * Options:
12
- * --path <dir> - Directory to scan (default: packages/)
13
- * --fix - Attempt to auto-fix issues (not implemented yet)
14
- */
15
-
16
- import { readFileSync } from "fs";
17
- import { glob } from "glob";
18
- import { dirname, relative, resolve } from "path";
19
- import { fileURLToPath } from "url";
20
-
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = dirname(__filename);
23
-
24
- interface StringMatch {
25
- file: string;
26
- line: number;
27
- context: string;
28
- string: string;
29
- suggestedKey: string | null;
30
- }
31
-
32
- /**
33
- * Load all English translations
34
- */
35
- function loadEnglishTranslations(): Record<string, string> {
36
- const basePath = resolve(__dirname, "../translations/en");
37
- const translations: Record<string, string> = {};
38
-
39
- try {
40
- const common = JSON.parse(
41
- readFileSync(resolve(basePath, "common.json"), "utf-8"),
42
- );
43
- const toolkit = JSON.parse(
44
- readFileSync(resolve(basePath, "toolkit.json"), "utf-8"),
45
- );
46
- const tools = JSON.parse(
47
- readFileSync(resolve(basePath, "tools.json"), "utf-8"),
48
- );
49
-
50
- // Flatten all translations
51
- flattenObject(common, "", translations);
52
- flattenObject(toolkit, "", translations);
53
- flattenObject(tools, "", translations);
54
- } catch (error) {
55
- console.error("Error loading English translations:", error);
56
- throw error;
57
- }
58
-
59
- return translations;
60
- }
61
-
62
- /**
63
- * Flatten nested object to dot notation
64
- */
65
- function flattenObject(
66
- obj: any,
67
- prefix: string,
68
- result: Record<string, string>,
69
- ) {
70
- for (const [key, value] of Object.entries(obj)) {
71
- const fullKey = prefix ? `${prefix}.${key}` : key;
72
-
73
- if (typeof value === "object" && value !== null) {
74
- // Check for plural form
75
- if ("one" in value && "other" in value) {
76
- result[fullKey] = value.one as string;
77
- } else {
78
- flattenObject(value, fullKey, result);
79
- }
80
- } else if (typeof value === "string") {
81
- result[fullKey] = value;
82
- }
83
- }
84
- }
85
-
86
- /**
87
- * Find translation key for a given string value
88
- */
89
- function findTranslationKey(
90
- translations: Record<string, string>,
91
- searchValue: string,
92
- ): string | null {
93
- // Exact match first
94
- for (const [key, value] of Object.entries(translations)) {
95
- if (value === searchValue) {
96
- return key;
97
- }
98
- }
99
-
100
- // Fuzzy match (case-insensitive)
101
- const lowerSearch = searchValue.toLowerCase();
102
- for (const [key, value] of Object.entries(translations)) {
103
- if (value.toLowerCase() === lowerSearch) {
104
- return key;
105
- }
106
- }
107
-
108
- return null;
109
- }
110
-
111
- /**
112
- * Scan a file for hardcoded strings
113
- */
114
- function scanFile(
115
- filePath: string,
116
- translations: Record<string, string>,
117
- ): StringMatch[] {
118
- const content = readFileSync(filePath, "utf-8");
119
- const lines = content.split("\n");
120
- const matches: StringMatch[] = [];
121
-
122
- // Patterns to match quoted English strings
123
- // Matches: "Text", 'Text', but ignores: i18n.t(...), class="...", etc.
124
- const stringPattern = /["']([A-Z][A-Za-z\s,.:;!?'\-()]+)["']/g;
125
-
126
- // Patterns to exclude (already using i18n, CSS classes, imports, etc.)
127
- const excludePatterns = [
128
- /i18n\./,
129
- /import\s+/,
130
- /from\s+['"]/,
131
- /class[:=]/,
132
- /className[:=]/,
133
- /aria-\w+[:=]/,
134
- /data-\w+[:=]/,
135
- /id[:=]/,
136
- /key[:=]/,
137
- /name[:=]/,
138
- /type[:=]/,
139
- /href[:=]/,
140
- /src[:=]/,
141
- /alt[:=]/,
142
- /title[:=]/,
143
- /placeholder[:=]/,
144
- /console\./,
145
- /\/\//, // Comments
146
- /\/\*/, // Block comments
147
- ];
148
-
149
- lines.forEach((line, index) => {
150
- // Skip lines that match exclude patterns
151
- if (excludePatterns.some((pattern) => pattern.test(line))) {
152
- return;
153
- }
154
-
155
- // Find all string matches in the line
156
- let match;
157
- while ((match = stringPattern.exec(line)) !== null) {
158
- const string = match[1];
159
-
160
- // Skip very short strings (likely not user-facing)
161
- if (string.length < 3) continue;
162
-
163
- // Skip strings that are mostly numbers or special characters
164
- if (!/[a-zA-Z]{3,}/.test(string)) continue;
165
-
166
- // Find corresponding translation key
167
- const suggestedKey = findTranslationKey(translations, string);
168
-
169
- matches.push({
170
- file: filePath,
171
- line: index + 1,
172
- context: line.trim(),
173
- string,
174
- suggestedKey,
175
- });
176
- }
177
- });
178
-
179
- return matches;
180
- }
181
-
182
- /**
183
- * Format scan results
184
- */
185
- function formatResults(
186
- matchesByFile: Map<string, StringMatch[]>,
187
- rootDir: string,
188
- ): string {
189
- let report = "\n";
190
- report += "┌─────────────────────────────────────────────────────┐\n";
191
- report += "│ Hardcoded String Scanner │\n";
192
- report += "└─────────────────────────────────────────────────────┘\n\n";
193
-
194
- const fileCount = matchesByFile.size;
195
- const totalMatches = Array.from(matchesByFile.values()).reduce(
196
- (sum, m) => sum + m.length,
197
- 0,
198
- );
199
-
200
- if (fileCount === 0) {
201
- report += "✅ No hardcoded strings found!\n\n";
202
- return report;
203
- }
204
-
205
- // Sort files by number of matches (descending)
206
- const sortedFiles = Array.from(matchesByFile.entries()).sort(
207
- (a, b) => b[1].length - a[1].length,
208
- );
209
-
210
- for (const [file, matches] of sortedFiles) {
211
- const relPath = relative(rootDir, file);
212
- report += `📄 ${relPath} (${matches.length} match${matches.length === 1 ? "" : "es"})\n`;
213
- report += "────────────────────────────────────────────────────────\n";
214
-
215
- for (const match of matches) {
216
- report += ` Line ${match.line}: ${match.context}\n`;
217
- report += ` Found: "${match.string}"\n`;
218
-
219
- if (match.suggestedKey) {
220
- report += ` Use: i18n?.t('${match.suggestedKey}') ?? '${match.suggestedKey}'\n`;
221
- } else {
222
- report += ` Note: No matching translation key found. Consider adding to translations.\n`;
223
- }
224
-
225
- report += "\n";
226
- }
227
-
228
- report += "\n";
229
- }
230
-
231
- report += "════════════════════════════════════════════════════════\n";
232
- report += "📊 Summary:\n";
233
- report += "────────────────────────────────────────────────────────\n";
234
- report += ` Total files scanned: ${fileCount}\n`;
235
- report += ` Files with matches: ${fileCount}\n`;
236
- report += ` Total hardcoded strings: ${totalMatches}\n\n`;
237
- report += "⚠️ Recommendation: Replace hardcoded strings with i18n keys\n";
238
- report += " for proper internationalization support.\n";
239
- report += "════════════════════════════════════════════════════════\n";
240
-
241
- return report;
242
- }
243
-
244
- /**
245
- * Find project root by looking for package.json with workspaces
246
- */
247
- function findProjectRoot(): string {
248
- let currentDir = process.cwd();
249
- while (currentDir !== "/") {
250
- try {
251
- const pkgPath = resolve(currentDir, "package.json");
252
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
253
- if (pkg.workspaces) {
254
- return currentDir;
255
- }
256
- } catch {
257
- // Continue searching
258
- }
259
- currentDir = dirname(currentDir);
260
- }
261
- // Fallback to script location's root
262
- return resolve(__dirname, "../../../..");
263
- }
264
-
265
- /**
266
- * Main entry point
267
- */
268
- async function main() {
269
- const args = process.argv.slice(2);
270
- const pathArg = args.indexOf("--path");
271
-
272
- // Find project root and default to scanning packages/
273
- const projectRoot = findProjectRoot();
274
- const defaultPath = resolve(projectRoot, "packages/");
275
- const scanPath =
276
- pathArg !== -1 ? resolve(projectRoot, args[pathArg + 1]) : defaultPath;
277
-
278
- console.log("🔍 Scanning for hardcoded strings...\n");
279
- console.log(`📂 Scan path: ${scanPath}\n`);
280
-
281
- try {
282
- // Load English translations for matching
283
- const translations = loadEnglishTranslations();
284
- console.log(
285
- `📖 Loaded ${Object.keys(translations).length} translation keys\n`,
286
- );
287
-
288
- // Find all component files
289
- const allFiles = await glob(`${scanPath}/**/*.{svelte,ts,tsx}`, {
290
- absolute: true,
291
- });
292
-
293
- // Filter out unwanted directories manually
294
- const files = allFiles.filter((file) => {
295
- return (
296
- !file.includes("/node_modules/") &&
297
- !file.includes("/dist/") &&
298
- !file.includes("/.svelte-kit/") &&
299
- !file.includes("/build/") &&
300
- !file.endsWith(".spec.ts") &&
301
- !file.endsWith(".test.ts") &&
302
- !file.includes("/scripts/")
303
- );
304
- });
305
-
306
- console.log(`📁 Found ${files.length} files to scan\n`);
307
-
308
- // Scan all files
309
- const matchesByFile = new Map<string, StringMatch[]>();
310
-
311
- for (const file of files) {
312
- const matches = scanFile(file, translations);
313
- if (matches.length > 0) {
314
- matchesByFile.set(file, matches);
315
- }
316
- }
317
-
318
- // Print results
319
- const report = formatResults(matchesByFile, projectRoot);
320
- console.log(report);
321
-
322
- // Exit with warning if matches found
323
- if (matchesByFile.size > 0) {
324
- console.warn(
325
- "\n⚠️ Found hardcoded strings. Consider using i18n for these strings.\n",
326
- );
327
- // Don't exit with error - this is informational only
328
- process.exit(0);
329
- }
330
-
331
- console.log("✅ No hardcoded strings found!\n");
332
- process.exit(0);
333
- } catch (error) {
334
- console.error("\n❌ Fatal error during scan:", error);
335
- process.exit(1);
336
- }
337
- }
338
-
339
- // Run if executed directly
340
- if (import.meta.url === `file://${process.argv[1]}`) {
341
- main();
342
- }