@pie-players/pie-players-shared 0.2.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 (233) hide show
  1. package/dist/config/profile.d.ts +15 -0
  2. package/dist/config/profile.d.ts.map +1 -0
  3. package/dist/config/profile.js +27 -0
  4. package/dist/config/profile.js.map +1 -0
  5. package/dist/i18n/index.d.ts +13 -0
  6. package/dist/i18n/index.d.ts.map +1 -0
  7. package/dist/i18n/index.js +12 -0
  8. package/dist/i18n/index.js.map +1 -0
  9. package/dist/i18n/loader.d.ts +36 -0
  10. package/dist/i18n/loader.d.ts.map +1 -0
  11. package/dist/i18n/loader.js +133 -0
  12. package/dist/i18n/loader.js.map +1 -0
  13. package/dist/i18n/scripts/check-coverage.d.ts +16 -0
  14. package/dist/i18n/scripts/check-coverage.d.ts.map +1 -0
  15. package/dist/i18n/scripts/check-coverage.js +262 -0
  16. package/dist/i18n/scripts/check-coverage.js.map +1 -0
  17. package/dist/i18n/scripts/scan-hardcoded.d.ts +16 -0
  18. package/dist/i18n/scripts/scan-hardcoded.d.ts.map +1 -0
  19. package/dist/i18n/scripts/scan-hardcoded.js +266 -0
  20. package/dist/i18n/scripts/scan-hardcoded.js.map +1 -0
  21. package/dist/i18n/simple-i18n.d.ts +69 -0
  22. package/dist/i18n/simple-i18n.d.ts.map +1 -0
  23. package/dist/i18n/simple-i18n.js +199 -0
  24. package/dist/i18n/simple-i18n.js.map +1 -0
  25. package/dist/i18n/translations/ar/common.json +36 -0
  26. package/dist/i18n/translations/ar/toolkit.json +48 -0
  27. package/dist/i18n/translations/ar/tools.json +109 -0
  28. package/dist/i18n/translations/en/common.json +36 -0
  29. package/dist/i18n/translations/en/toolkit.json +48 -0
  30. package/dist/i18n/translations/en/tools.json +109 -0
  31. package/dist/i18n/translations/es/common.json +36 -0
  32. package/dist/i18n/translations/es/toolkit.json +48 -0
  33. package/dist/i18n/translations/es/tools.json +109 -0
  34. package/dist/i18n/translations/zh/common.json +36 -0
  35. package/dist/i18n/translations/zh/toolkit.json +48 -0
  36. package/dist/i18n/translations/zh/tools.json +109 -0
  37. package/dist/i18n/types.d.ts +58 -0
  38. package/dist/i18n/types.d.ts.map +1 -0
  39. package/dist/i18n/types.js +8 -0
  40. package/dist/i18n/types.js.map +1 -0
  41. package/dist/i18n/use-i18n-standalone.svelte.d.ts +87 -0
  42. package/dist/i18n/use-i18n-standalone.svelte.d.ts.map +1 -0
  43. package/dist/i18n/use-i18n-standalone.svelte.js +151 -0
  44. package/dist/i18n/use-i18n-standalone.svelte.js.map +1 -0
  45. package/dist/i18n/use-i18n.svelte.d.ts +67 -0
  46. package/dist/i18n/use-i18n.svelte.d.ts.map +1 -0
  47. package/dist/i18n/use-i18n.svelte.js +144 -0
  48. package/dist/i18n/use-i18n.svelte.js.map +1 -0
  49. package/dist/index.d.ts +11 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +11 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/instrumentation/index.d.ts +53 -0
  54. package/dist/instrumentation/index.d.ts.map +1 -0
  55. package/dist/instrumentation/index.js +53 -0
  56. package/dist/instrumentation/index.js.map +1 -0
  57. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +197 -0
  58. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts.map +1 -0
  59. package/dist/instrumentation/providers/BaseInstrumentationProvider.js +267 -0
  60. package/dist/instrumentation/providers/BaseInstrumentationProvider.js.map +1 -0
  61. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts +106 -0
  62. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts.map +1 -0
  63. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js +182 -0
  64. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js.map +1 -0
  65. package/dist/instrumentation/providers/DataDogInstrumentationProvider.d.ts +170 -0
  66. package/dist/instrumentation/providers/DataDogInstrumentationProvider.d.ts.map +1 -0
  67. package/dist/instrumentation/providers/DataDogInstrumentationProvider.js +183 -0
  68. package/dist/instrumentation/providers/DataDogInstrumentationProvider.js.map +1 -0
  69. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +86 -0
  70. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts.map +1 -0
  71. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js +135 -0
  72. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js.map +1 -0
  73. package/dist/instrumentation/providers/index.d.ts +12 -0
  74. package/dist/instrumentation/providers/index.d.ts.map +1 -0
  75. package/dist/instrumentation/providers/index.js +12 -0
  76. package/dist/instrumentation/providers/index.js.map +1 -0
  77. package/dist/instrumentation/types.d.ts +348 -0
  78. package/dist/instrumentation/types.d.ts.map +1 -0
  79. package/dist/instrumentation/types.js +9 -0
  80. package/dist/instrumentation/types.js.map +1 -0
  81. package/dist/loader-config.d.ts +76 -0
  82. package/dist/loader-config.d.ts.map +1 -0
  83. package/dist/loader-config.js +12 -0
  84. package/dist/loader-config.js.map +1 -0
  85. package/dist/loaders/ElementLoader.d.ts +72 -0
  86. package/dist/loaders/ElementLoader.d.ts.map +1 -0
  87. package/dist/loaders/ElementLoader.js +52 -0
  88. package/dist/loaders/ElementLoader.js.map +1 -0
  89. package/dist/loaders/EsmElementLoader.d.ts +67 -0
  90. package/dist/loaders/EsmElementLoader.d.ts.map +1 -0
  91. package/dist/loaders/EsmElementLoader.js +71 -0
  92. package/dist/loaders/EsmElementLoader.js.map +1 -0
  93. package/dist/loaders/IifeElementLoader.d.ts +61 -0
  94. package/dist/loaders/IifeElementLoader.d.ts.map +1 -0
  95. package/dist/loaders/IifeElementLoader.js +63 -0
  96. package/dist/loaders/IifeElementLoader.js.map +1 -0
  97. package/dist/loaders/index.d.ts +28 -0
  98. package/dist/loaders/index.d.ts.map +1 -0
  99. package/dist/loaders/index.js +25 -0
  100. package/dist/loaders/index.js.map +1 -0
  101. package/dist/object/index.d.ts +12 -0
  102. package/dist/object/index.d.ts.map +1 -0
  103. package/dist/object/index.js +40 -0
  104. package/dist/object/index.js.map +1 -0
  105. package/dist/pie/asset-handler.d.ts +64 -0
  106. package/dist/pie/asset-handler.d.ts.map +1 -0
  107. package/dist/pie/asset-handler.js +238 -0
  108. package/dist/pie/asset-handler.js.map +1 -0
  109. package/dist/pie/component-context.d.ts +22 -0
  110. package/dist/pie/component-context.d.ts.map +1 -0
  111. package/dist/pie/component-context.js +30 -0
  112. package/dist/pie/component-context.js.map +1 -0
  113. package/dist/pie/config.d.ts +39 -0
  114. package/dist/pie/config.d.ts.map +1 -0
  115. package/dist/pie/config.js +174 -0
  116. package/dist/pie/config.js.map +1 -0
  117. package/dist/pie/configure-initialization.d.ts +35 -0
  118. package/dist/pie/configure-initialization.d.ts.map +1 -0
  119. package/dist/pie/configure-initialization.js +141 -0
  120. package/dist/pie/configure-initialization.js.map +1 -0
  121. package/dist/pie/esm-loader.d.ts +93 -0
  122. package/dist/pie/esm-loader.d.ts.map +1 -0
  123. package/dist/pie/esm-loader.js +308 -0
  124. package/dist/pie/esm-loader.js.map +1 -0
  125. package/dist/pie/iife-loader.d.ts +76 -0
  126. package/dist/pie/iife-loader.d.ts.map +1 -0
  127. package/dist/pie/iife-loader.js +303 -0
  128. package/dist/pie/iife-loader.js.map +1 -0
  129. package/dist/pie/index.d.ts +31 -0
  130. package/dist/pie/index.d.ts.map +1 -0
  131. package/dist/pie/index.js +34 -0
  132. package/dist/pie/index.js.map +1 -0
  133. package/dist/pie/initialization.d.ts +40 -0
  134. package/dist/pie/initialization.d.ts.map +1 -0
  135. package/dist/pie/initialization.js +349 -0
  136. package/dist/pie/initialization.js.map +1 -0
  137. package/dist/pie/logger.d.ts +64 -0
  138. package/dist/pie/logger.d.ts.map +1 -0
  139. package/dist/pie/logger.js +45 -0
  140. package/dist/pie/logger.js.map +1 -0
  141. package/dist/pie/math-rendering.d.ts +69 -0
  142. package/dist/pie/math-rendering.d.ts.map +1 -0
  143. package/dist/pie/math-rendering.js +98 -0
  144. package/dist/pie/math-rendering.js.map +1 -0
  145. package/dist/pie/overrides.d.ts +43 -0
  146. package/dist/pie/overrides.d.ts.map +1 -0
  147. package/dist/pie/overrides.js +146 -0
  148. package/dist/pie/overrides.js.map +1 -0
  149. package/dist/pie/player-initializer.d.ts +55 -0
  150. package/dist/pie/player-initializer.d.ts.map +1 -0
  151. package/dist/pie/player-initializer.js +123 -0
  152. package/dist/pie/player-initializer.js.map +1 -0
  153. package/dist/pie/registry.d.ts +11 -0
  154. package/dist/pie/registry.d.ts.map +1 -0
  155. package/dist/pie/registry.js +21 -0
  156. package/dist/pie/registry.js.map +1 -0
  157. package/dist/pie/resource-monitor.d.ts +208 -0
  158. package/dist/pie/resource-monitor.d.ts.map +1 -0
  159. package/dist/pie/resource-monitor.js +969 -0
  160. package/dist/pie/resource-monitor.js.map +1 -0
  161. package/dist/pie/scoring.d.ts +17 -0
  162. package/dist/pie/scoring.d.ts.map +1 -0
  163. package/dist/pie/scoring.js +84 -0
  164. package/dist/pie/scoring.js.map +1 -0
  165. package/dist/pie/types.d.ts +136 -0
  166. package/dist/pie/types.d.ts.map +1 -0
  167. package/dist/pie/types.js +52 -0
  168. package/dist/pie/types.js.map +1 -0
  169. package/dist/pie/updates.d.ts +20 -0
  170. package/dist/pie/updates.d.ts.map +1 -0
  171. package/dist/pie/updates.js +175 -0
  172. package/dist/pie/updates.js.map +1 -0
  173. package/dist/pie/use-resource-monitor.svelte.d.ts +56 -0
  174. package/dist/pie/use-resource-monitor.svelte.d.ts.map +1 -0
  175. package/dist/pie/use-resource-monitor.svelte.js +117 -0
  176. package/dist/pie/use-resource-monitor.svelte.js.map +1 -0
  177. package/dist/pie/utils.d.ts +44 -0
  178. package/dist/pie/utils.d.ts.map +1 -0
  179. package/dist/pie/utils.js +74 -0
  180. package/dist/pie/utils.js.map +1 -0
  181. package/dist/types/custom-elements.d.ts +183 -0
  182. package/dist/types/custom-elements.d.ts.map +1 -0
  183. package/dist/types/custom-elements.js +8 -0
  184. package/dist/types/custom-elements.js.map +1 -0
  185. package/dist/types/index.d.ts +761 -0
  186. package/dist/types/index.d.ts.map +1 -0
  187. package/dist/types/index.js +120 -0
  188. package/dist/types/index.js.map +1 -0
  189. package/dist/types/search.d.ts +105 -0
  190. package/dist/types/search.d.ts.map +1 -0
  191. package/dist/types/search.js +12 -0
  192. package/dist/types/search.js.map +1 -0
  193. package/dist/types/transform.d.ts +48 -0
  194. package/dist/types/transform.d.ts.map +1 -0
  195. package/dist/types/transform.js +21 -0
  196. package/dist/types/transform.js.map +1 -0
  197. package/dist/ui/focus-trap.d.ts +10 -0
  198. package/dist/ui/focus-trap.d.ts.map +1 -0
  199. package/dist/ui/focus-trap.js +30 -0
  200. package/dist/ui/focus-trap.js.map +1 -0
  201. package/dist/ui/safe-storage.d.ts +3 -0
  202. package/dist/ui/safe-storage.d.ts.map +1 -0
  203. package/dist/ui/safe-storage.js +21 -0
  204. package/dist/ui/safe-storage.js.map +1 -0
  205. package/package.json +118 -0
  206. package/src/components/PieItemPlayer.svelte +604 -0
  207. package/src/components/PiePreviewLayout.svelte +144 -0
  208. package/src/components/PiePreviewToggle.svelte +110 -0
  209. package/src/components/PieSpinner.svelte +85 -0
  210. package/src/components/ToolSettingsButton.svelte +31 -0
  211. package/src/components/ToolSettingsPanel.svelte +90 -0
  212. package/src/components/index.ts +6 -0
  213. package/src/i18n/README.md +223 -0
  214. package/src/i18n/index.ts +26 -0
  215. package/src/i18n/loader.ts +156 -0
  216. package/src/i18n/scripts/check-coverage.ts +345 -0
  217. package/src/i18n/scripts/scan-hardcoded.ts +342 -0
  218. package/src/i18n/simple-i18n.ts +236 -0
  219. package/src/i18n/translations/ar/common.json +36 -0
  220. package/src/i18n/translations/ar/toolkit.json +48 -0
  221. package/src/i18n/translations/ar/tools.json +109 -0
  222. package/src/i18n/translations/en/common.json +36 -0
  223. package/src/i18n/translations/en/toolkit.json +48 -0
  224. package/src/i18n/translations/en/tools.json +109 -0
  225. package/src/i18n/translations/es/common.json +36 -0
  226. package/src/i18n/translations/es/toolkit.json +48 -0
  227. package/src/i18n/translations/es/tools.json +109 -0
  228. package/src/i18n/translations/zh/common.json +36 -0
  229. package/src/i18n/translations/zh/toolkit.json +48 -0
  230. package/src/i18n/translations/zh/tools.json +109 -0
  231. package/src/i18n/types.ts +66 -0
  232. package/src/i18n/use-i18n-standalone.svelte.ts +184 -0
  233. package/src/i18n/use-i18n.svelte.ts +163 -0
@@ -0,0 +1,342 @@
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
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Simple I18n Implementation
3
+ *
4
+ * Lightweight i18n without external dependencies.
5
+ * Used for standalone components that don't need the full service architecture.
6
+ */
7
+
8
+ import type { I18nConfig, PluralTranslation, TranslationBundle } from "./types";
9
+
10
+ /**
11
+ * Simple I18n class for standalone use
12
+ */
13
+ export class SimpleI18n {
14
+ private locale: string = "en";
15
+ private fallbackLocale: string = "en";
16
+ private direction: "ltr" | "rtl" = "ltr";
17
+ private translations = new Map<string, TranslationBundle>();
18
+ private listeners = new Set<() => void>();
19
+ private loadingPromises = new Map<string, Promise<void>>();
20
+ private config: I18nConfig;
21
+
22
+ constructor(config: I18nConfig = {}) {
23
+ this.config = config;
24
+ this.fallbackLocale = config.fallbackLocale || "en";
25
+
26
+ // Load bundled translations
27
+ if (config.bundledTranslations) {
28
+ for (const [locale, bundle] of Object.entries(
29
+ config.bundledTranslations,
30
+ )) {
31
+ this.translations.set(locale, bundle);
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Initialize with locale
38
+ */
39
+ async initialize(config: I18nConfig): Promise<void> {
40
+ Object.assign(this.config, config);
41
+
42
+ const locale = config.locale || this.detectBrowserLocale();
43
+ await this.setLocale(locale);
44
+ }
45
+
46
+ /**
47
+ * Translate a key
48
+ */
49
+ t(key: string, params?: Record<string, any>): string {
50
+ const translation = this.getTranslation(key);
51
+ return this.interpolate(translation as string, params);
52
+ }
53
+
54
+ /**
55
+ * Translate with pluralization
56
+ */
57
+ tn(key: string, count: number, params?: Record<string, any>): string {
58
+ const translation = this.getTranslation(key);
59
+
60
+ if (typeof translation === "object") {
61
+ const pluralForm = this.selectPluralForm(count, this.locale);
62
+ const text = translation[pluralForm] || translation.other;
63
+ return this.interpolate(text, { ...params, count });
64
+ }
65
+
66
+ return this.interpolate(translation as string, { ...params, count });
67
+ }
68
+
69
+ /**
70
+ * Get current locale
71
+ */
72
+ getLocale(): string {
73
+ return this.locale;
74
+ }
75
+
76
+ /**
77
+ * Change locale
78
+ */
79
+ async setLocale(locale: string): Promise<void> {
80
+ // Check if already loading
81
+ if (this.loadingPromises.has(locale)) {
82
+ await this.loadingPromises.get(locale);
83
+ return;
84
+ }
85
+
86
+ // Check if already loaded
87
+ if (this.translations.has(locale)) {
88
+ this.applyLocale(locale);
89
+ return;
90
+ }
91
+
92
+ // Load translations
93
+ const loadingPromise = this.loadTranslationsForLocale(locale);
94
+ this.loadingPromises.set(locale, loadingPromise);
95
+
96
+ try {
97
+ await loadingPromise;
98
+ this.applyLocale(locale);
99
+ } finally {
100
+ this.loadingPromises.delete(locale);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get current direction
106
+ */
107
+ getDirection(): "ltr" | "rtl" {
108
+ return this.direction;
109
+ }
110
+
111
+ /**
112
+ * Get available locales
113
+ */
114
+ getAvailableLocales(): string[] {
115
+ return Array.from(this.translations.keys());
116
+ }
117
+
118
+ /**
119
+ * Check if locale is loaded
120
+ */
121
+ isLocaleLoaded(locale: string): boolean {
122
+ return this.translations.has(locale);
123
+ }
124
+
125
+ /**
126
+ * Subscribe to changes
127
+ */
128
+ subscribe(listener: () => void): () => void {
129
+ this.listeners.add(listener);
130
+ return () => {
131
+ this.listeners.delete(listener);
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Check if key exists
137
+ */
138
+ hasKey(key: string): boolean {
139
+ const bundle = this.translations.get(this.locale);
140
+ return !!bundle?.translations[key];
141
+ }
142
+
143
+ private notifyListeners(): void {
144
+ for (const listener of this.listeners) {
145
+ listener();
146
+ }
147
+ }
148
+
149
+ private applyLocale(locale: string): void {
150
+ const bundle = this.translations.get(locale);
151
+ if (!bundle) return;
152
+
153
+ this.locale = locale;
154
+ this.direction = bundle.direction;
155
+
156
+ this.applyDOMDirection();
157
+ this.notifyListeners();
158
+ }
159
+
160
+ private applyDOMDirection(): void {
161
+ if (typeof document === "undefined") return;
162
+
163
+ document.documentElement.setAttribute("dir", this.direction);
164
+ document.documentElement.setAttribute("lang", this.locale);
165
+ }
166
+
167
+ private async loadTranslationsForLocale(locale: string): Promise<void> {
168
+ if (!this.config.loadTranslations) {
169
+ throw new Error(`No translation loader configured for locale: ${locale}`);
170
+ }
171
+
172
+ const bundle = await this.config.loadTranslations(locale);
173
+ this.translations.set(locale, bundle);
174
+ }
175
+
176
+ private getTranslation(key: string): string | PluralTranslation {
177
+ // Try current locale
178
+ const currentBundle = this.translations.get(this.locale);
179
+ if (currentBundle?.translations[key]) {
180
+ return currentBundle.translations[key];
181
+ }
182
+
183
+ // Try fallback locale
184
+ const fallbackBundle = this.translations.get(this.fallbackLocale);
185
+ if (fallbackBundle?.translations[key]) {
186
+ return fallbackBundle.translations[key];
187
+ }
188
+
189
+ // Missing key
190
+ if (this.config.onMissingKey) {
191
+ this.config.onMissingKey(key, this.locale);
192
+ }
193
+
194
+ return key;
195
+ }
196
+
197
+ private interpolate(text: string, params?: Record<string, any>): string {
198
+ if (!params) return text;
199
+
200
+ return text.replace(/\{(\w+)\}/g, (match, key) => {
201
+ return params[key]?.toString() || match;
202
+ });
203
+ }
204
+
205
+ private selectPluralForm(
206
+ count: number,
207
+ locale: string,
208
+ ): keyof PluralTranslation {
209
+ // Use Intl.PluralRules if available
210
+ if (typeof Intl !== "undefined" && Intl.PluralRules) {
211
+ try {
212
+ const rules = new Intl.PluralRules(locale);
213
+ const category = rules.select(count);
214
+ return category as keyof PluralTranslation;
215
+ } catch {
216
+ // Fall through to simple rules
217
+ }
218
+ }
219
+
220
+ // Fallback to simple rules
221
+ if (count === 0) return "zero";
222
+ if (count === 1) return "one";
223
+ return "other";
224
+ }
225
+
226
+ private detectBrowserLocale(): string {
227
+ if (typeof navigator === "undefined") return "en";
228
+
229
+ const browserLang =
230
+ navigator.language ||
231
+ (navigator.languages && navigator.languages[0]) ||
232
+ "en";
233
+
234
+ return browserLang.split("-")[0];
235
+ }
236
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "common": {
3
+ "save": "حفظ",
4
+ "cancel": "إلغاء",
5
+ "close": "إغلاق",
6
+ "loading": "جار التحميل...",
7
+ "error": "خطأ",
8
+ "retry": "إعادة المحاولة",
9
+ "back": "رجوع",
10
+ "next": "التالي",
11
+ "previous": "السابق",
12
+ "submit": "إرسال",
13
+ "continue": "متابعة",
14
+ "finish": "إنهاء",
15
+ "yes": "نعم",
16
+ "no": "لا",
17
+ "ok": "موافق",
18
+ "settings": "الإعدادات",
19
+ "language": "اللغة",
20
+ "theme": "المظهر",
21
+ "settings_aria": "فتح قائمة الإعدادات",
22
+ "rtl_notice": "اتجاه النص من اليمين إلى اليسار نشط",
23
+ "question": {
24
+ "one": "سؤال",
25
+ "other": "أسئلة"
26
+ },
27
+ "item": {
28
+ "one": "عنصر",
29
+ "other": "عناصر"
30
+ },
31
+ "character": {
32
+ "one": "حرف",
33
+ "other": "حروف"
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "assessment": {
3
+ "title": "التقييم",
4
+ "questions": {
5
+ "one": "{count} سؤال",
6
+ "other": "{count} أسئلة"
7
+ },
8
+ "question_of": "السؤال {current} من {total}",
9
+ "student_name": "اسم الطالب",
10
+ "fullscreen": "ملء الشاشة",
11
+ "exit_fullscreen": "الخروج من ملء الشاشة",
12
+ "enter_fullscreen_aria": "الدخول إلى ملء الشاشة",
13
+ "exit_fullscreen_aria": "الخروج من ملء الشاشة"
14
+ },
15
+ "accommodation": {
16
+ "audio": "صوت",
17
+ "audio_aria": "تبديل الصوت/تحويل النص إلى كلام",
18
+ "contrast": "التباين",
19
+ "contrast_aria": "تبديل تباين الألوان"
20
+ },
21
+ "navigation": {
22
+ "back": "← رجوع",
23
+ "next": "التالي ←",
24
+ "previous": "السابق",
25
+ "submit": "إرسال",
26
+ "navigate_to": "الانتقال إلى السؤال {index}",
27
+ "section": "القسم"
28
+ },
29
+ "section": {
30
+ "title": "بنية التقييم",
31
+ "add": "+ إضافة قسم",
32
+ "untitled": "قسم بدون عنوان",
33
+ "remove": "حذف القسم",
34
+ "items": {
35
+ "one": "{count} عنصر",
36
+ "other": "{count} عناصر"
37
+ },
38
+ "aria_label": "القسم: {title}",
39
+ "drop_zone": "إسقاط العناصر هنا",
40
+ "unassigned": "عناصر غير مخصصة",
41
+ "unassigned_instruction": "اسحب هذه العناصر إلى الأقسام",
42
+ "unassigned_item_aria": "سؤال غير مخصص: {title}"
43
+ },
44
+ "item": {
45
+ "load_error": "تعذر تحميل السؤال.",
46
+ "untitled": "بدون عنوان"
47
+ }
48
+ }