@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,26 @@
1
+ /**
2
+ * I18n Module
3
+ *
4
+ * Internationalization system for PIE Players.
5
+ *
6
+ * @module @pie-players/pie-players-shared/i18n
7
+ */
8
+
9
+ export {
10
+ BUNDLED_TRANSLATIONS,
11
+ getAvailableLocales,
12
+ isRTLLocale,
13
+ loadTranslations,
14
+ } from "./loader";
15
+ export { SimpleI18n } from "./simple-i18n";
16
+ export type {
17
+ I18nConfig,
18
+ II18nService,
19
+ PluralTranslation,
20
+ TranslationBundle,
21
+ } from "./types";
22
+ export { useI18n } from "./use-i18n.svelte";
23
+ export {
24
+ type UseI18nStandaloneConfig,
25
+ useI18nStandalone,
26
+ } from "./use-i18n-standalone.svelte";
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Translation Loader
3
+ *
4
+ * Implements hybrid loading strategy:
5
+ * - English bundled with application
6
+ * - Other languages loaded on demand
7
+ *
8
+ * Part of PIE Players i18n system.
9
+ */
10
+
11
+ // Import English translations (bundled)
12
+ import enCommon from "./translations/en/common.json";
13
+ import enToolkit from "./translations/en/toolkit.json";
14
+ import enTools from "./translations/en/tools.json";
15
+ import type { TranslationBundle } from "./types";
16
+
17
+ // RTL locales
18
+ const RTL_LOCALES = ["ar", "he", "fa", "ur"];
19
+
20
+ /**
21
+ * Bundled English translations
22
+ * Available immediately without network request
23
+ */
24
+ export const BUNDLED_TRANSLATIONS: Record<string, TranslationBundle> = {
25
+ en: {
26
+ locale: "en",
27
+ direction: "ltr",
28
+ translations: {
29
+ ...flattenTranslations(enCommon),
30
+ ...flattenTranslations(enToolkit),
31
+ ...flattenTranslations(enTools),
32
+ },
33
+ },
34
+ };
35
+
36
+ const LOCALE_IMPORTS: Record<string, () => Promise<[any, any, any]>> = {
37
+ es: () =>
38
+ Promise.all([
39
+ import("./translations/es/common.json"),
40
+ import("./translations/es/toolkit.json"),
41
+ import("./translations/es/tools.json"),
42
+ ]),
43
+ zh: () =>
44
+ Promise.all([
45
+ import("./translations/zh/common.json"),
46
+ import("./translations/zh/toolkit.json"),
47
+ import("./translations/zh/tools.json"),
48
+ ]),
49
+ ar: () =>
50
+ Promise.all([
51
+ import("./translations/ar/common.json"),
52
+ import("./translations/ar/toolkit.json"),
53
+ import("./translations/ar/tools.json"),
54
+ ]),
55
+ };
56
+
57
+ /**
58
+ * Load translations for a locale
59
+ *
60
+ * @param locale Locale code (e.g., 'en', 'es', 'zh', 'ar')
61
+ * @returns Translation bundle with flattened keys
62
+ */
63
+ export async function loadTranslations(
64
+ locale: string,
65
+ ): Promise<TranslationBundle> {
66
+ // Return bundled English immediately
67
+ if (locale === "en") {
68
+ return BUNDLED_TRANSLATIONS.en;
69
+ }
70
+
71
+ const importer = LOCALE_IMPORTS[locale];
72
+ if (!importer) {
73
+ throw new Error(
74
+ `Translation files not found for locale: ${locale}. Ensure translation files exist in packages/players-shared/src/i18n/translations/${locale}/`,
75
+ );
76
+ }
77
+
78
+ // Lazy load other locales
79
+ try {
80
+ const [common, toolkit, tools] = await importer();
81
+
82
+ return {
83
+ locale,
84
+ direction: RTL_LOCALES.includes(locale) ? "rtl" : "ltr",
85
+ translations: {
86
+ ...flattenTranslations(common.default || common),
87
+ ...flattenTranslations(toolkit.default || toolkit),
88
+ ...flattenTranslations(tools.default || tools),
89
+ },
90
+ };
91
+ } catch (error) {
92
+ console.error(`Failed to load translations for locale: ${locale}`, error);
93
+ throw new Error(
94
+ `Translation files not found for locale: ${locale}. Ensure translation files exist in packages/players-shared/src/i18n/translations/${locale}/`,
95
+ );
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Flatten nested JSON to dot notation
101
+ *
102
+ * Converts:
103
+ * { "common": { "save": "Save" } }
104
+ *
105
+ * To:
106
+ * { "common.save": "Save" }
107
+ *
108
+ * @param obj Nested translation object
109
+ * @param prefix Current key prefix
110
+ * @param result Accumulated result
111
+ * @returns Flattened translation object
112
+ */
113
+ function flattenTranslations(
114
+ obj: any,
115
+ prefix = "",
116
+ result: Record<string, any> = {},
117
+ ): Record<string, any> {
118
+ for (const [key, value] of Object.entries(obj)) {
119
+ const newKey = prefix ? `${prefix}.${key}` : key;
120
+
121
+ if (value && typeof value === "object" && !Array.isArray(value)) {
122
+ // Check if it's a plural form (has 'one' or 'other' keys)
123
+ if ("one" in value || "other" in value) {
124
+ // This is a plural translation, keep it as an object
125
+ result[newKey] = value;
126
+ } else {
127
+ // This is a nested object, flatten recursively
128
+ flattenTranslations(value, newKey, result);
129
+ }
130
+ } else {
131
+ // This is a leaf value
132
+ result[newKey] = value;
133
+ }
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ /**
140
+ * Get available locales
141
+ *
142
+ * @returns Array of locale codes
143
+ */
144
+ export function getAvailableLocales(): string[] {
145
+ return ["en", "es", "zh", "ar"];
146
+ }
147
+
148
+ /**
149
+ * Check if locale is RTL
150
+ *
151
+ * @param locale Locale code
152
+ * @returns True if locale is right-to-left
153
+ */
154
+ export function isRTLLocale(locale: string): boolean {
155
+ return RTL_LOCALES.includes(locale);
156
+ }
@@ -0,0 +1,345 @@
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
+ }