@pie-players/pie-players-shared 0.3.66 → 0.3.68

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 (89) hide show
  1. package/README.md +59 -0
  2. package/dist/formative/index.d.ts +16 -0
  3. package/dist/formative/index.js +5 -0
  4. package/dist/formative/mastery.d.ts +16 -0
  5. package/dist/formative/mastery.js +43 -0
  6. package/dist/formative/outcome.d.ts +19 -0
  7. package/dist/formative/outcome.js +90 -0
  8. package/dist/formative/policy.d.ts +37 -0
  9. package/dist/formative/policy.js +86 -0
  10. package/dist/formative/session.d.ts +19 -0
  11. package/dist/formative/session.js +113 -0
  12. package/dist/formative/state.d.ts +60 -0
  13. package/dist/formative/state.js +140 -0
  14. package/dist/formative/types.d.ts +164 -0
  15. package/dist/formative/types.js +13 -0
  16. package/dist/i18n/catalogs.d.ts +38 -0
  17. package/dist/i18n/catalogs.js +47 -0
  18. package/dist/i18n/index.d.ts +28 -4
  19. package/dist/i18n/index.js +34 -3
  20. package/dist/i18n/language-tags.d.ts +51 -0
  21. package/dist/i18n/language-tags.js +84 -0
  22. package/dist/i18n/messages/en-US.d.ts +601 -0
  23. package/dist/i18n/messages/en-US.js +617 -0
  24. package/dist/i18n/messages/nl-NL.d.ts +516 -0
  25. package/dist/i18n/messages/nl-NL.js +516 -0
  26. package/dist/i18n/provider.d.ts +168 -0
  27. package/dist/i18n/provider.js +472 -0
  28. package/dist/i18n/types.d.ts +177 -28
  29. package/dist/i18n/types.js +6 -2
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.js +2 -0
  32. package/dist/pie/custom-element-define.d.ts +24 -0
  33. package/dist/pie/custom-element-define.js +36 -6
  34. package/dist/pie/index.d.ts +1 -1
  35. package/dist/pie/index.js +1 -1
  36. package/dist/pie/versioned-tag.js +3 -2
  37. package/dist/security/sanitize-forbidden-lists.d.ts +8 -0
  38. package/dist/security/sanitize-forbidden-lists.js +41 -0
  39. package/dist/security/sanitize-item-markup.js +3 -36
  40. package/dist/security/sanitize-svg-icon.js +3 -25
  41. package/dist/security/wrap-overwide-images.d.ts +5 -10
  42. package/dist/security/wrap-overwide-images.js +13 -62
  43. package/dist/security/wrap-overwide-tables.d.ts +5 -10
  44. package/dist/security/wrap-overwide-tables.js +13 -62
  45. package/dist/security/wrap-overwide.d.ts +51 -0
  46. package/dist/security/wrap-overwide.js +91 -0
  47. package/dist/server/npm-auth-env.d.ts +13 -0
  48. package/dist/server/npm-auth-env.js +64 -0
  49. package/dist/timed-media/index.d.ts +17 -0
  50. package/dist/timed-media/index.js +16 -0
  51. package/dist/timed-media/media-element-source.d.ts +36 -0
  52. package/dist/timed-media/media-element-source.js +121 -0
  53. package/dist/timed-media/policy.d.ts +36 -0
  54. package/dist/timed-media/policy.js +297 -0
  55. package/dist/timed-media/session.d.ts +21 -0
  56. package/dist/timed-media/session.js +85 -0
  57. package/dist/timed-media/state.d.ts +66 -0
  58. package/dist/timed-media/state.js +357 -0
  59. package/dist/timed-media/types.d.ts +343 -0
  60. package/dist/timed-media/types.js +12 -0
  61. package/dist/tools/term-lookup.d.ts +189 -0
  62. package/dist/tools/term-lookup.js +284 -0
  63. package/dist/types/index.d.ts +105 -1
  64. package/dist/ui/attribute-coercion.d.ts +1 -0
  65. package/dist/ui/attribute-coercion.js +19 -0
  66. package/dist/ui/content-styles.d.ts +11 -3
  67. package/dist/ui/content-styles.js +42 -13
  68. package/dist/ui/first-focusable.d.ts +22 -0
  69. package/dist/ui/first-focusable.js +47 -0
  70. package/dist/ui/focus-trap.js +7 -2
  71. package/dist/ui/pointer-drag.d.ts +30 -0
  72. package/dist/ui/pointer-drag.js +34 -0
  73. package/package.json +35 -3
  74. package/dist/i18n/loader.d.ts +0 -35
  75. package/dist/i18n/loader.js +0 -132
  76. package/dist/i18n/simple-i18n.d.ts +0 -68
  77. package/dist/i18n/simple-i18n.js +0 -198
  78. package/dist/i18n/translations/ar/common.json +0 -36
  79. package/dist/i18n/translations/ar/toolkit.json +0 -48
  80. package/dist/i18n/translations/ar/tools.json +0 -103
  81. package/dist/i18n/translations/en/common.json +0 -36
  82. package/dist/i18n/translations/en/toolkit.json +0 -48
  83. package/dist/i18n/translations/en/tools.json +0 -103
  84. package/dist/i18n/translations/es/common.json +0 -36
  85. package/dist/i18n/translations/es/toolkit.json +0 -48
  86. package/dist/i18n/translations/es/tools.json +0 -103
  87. package/dist/i18n/translations/zh/common.json +0 -36
  88. package/dist/i18n/translations/zh/toolkit.json +0 -48
  89. package/dist/i18n/translations/zh/tools.json +0 -103
@@ -17,14 +17,15 @@
17
17
  *
18
18
  * The CSS text is passed in rather than imported here: this package builds with
19
19
  * plain `tsc`, so it cannot inline a stylesheet. Bundler-built player packages
20
- * import it with Vite's `?inline` and hand the text over.
20
+ * import it with Vite's `?raw` and hand the text over.
21
21
  */
22
22
  /** Marks a `<style>` element this module owns, and keeps installs idempotent. */
23
23
  const MARKER_ATTRIBUTE = "data-pie-content-styles";
24
24
  /**
25
25
  * Declared by `components.css` itself, so it is observable no matter how the
26
- * stylesheet arrived — our injection or a host import. Used only to tell a host
27
- * that opted out but then shipped nothing.
26
+ * stylesheet arrived — our injection or a host import. Diagnostics only: it
27
+ * tells a host that opted out and then shipped nothing, and it spots a host copy
28
+ * sitting alongside ours.
28
29
  */
29
30
  const SENTINEL_PROPERTY = "--pie-content-styles";
30
31
  // Svelte's dev-mode custom-element reset expands `all: unset` into individual
@@ -43,7 +44,22 @@ const declaresContentStylesSentinel = (rule) => {
43
44
  ?.getPropertyValue(SENTINEL_PROPERTY)
44
45
  .trim()
45
46
  .toLowerCase();
46
- return Boolean(value && !CSS_WIDE_RESET_VALUES.has(value));
47
+ if (value)
48
+ return !CSS_WIDE_RESET_VALUES.has(value);
49
+ // Grouping rules hold no declarations of their own, so the sentinel sits one
50
+ // or more levels down. A host that confines its copy — `@scope
51
+ // (.item-content) { … }`, `@layer pie-content { … }` — presents exactly one
52
+ // top-level rule with an empty `.style`, and a top-level-only scan reads that
53
+ // as "no copy here". That made both detection paths blind to the one host
54
+ // configuration this module most needs to recognise.
55
+ const nested = rule.cssRules;
56
+ if (!nested)
57
+ return false;
58
+ for (const child of Array.from(nested)) {
59
+ if (declaresContentStylesSentinel(child))
60
+ return true;
61
+ }
62
+ return false;
47
63
  };
48
64
  /** `<html data-pie-content-styles="host">` opts a host out of installation. */
49
65
  const OPT_OUT_ATTRIBUTE = "data-pie-content-styles";
@@ -59,8 +75,16 @@ export function contentStylesOptedOut() {
59
75
  return (document.documentElement.getAttribute(OPT_OUT_ATTRIBUTE) === OPT_OUT_VALUE);
60
76
  }
61
77
  /**
62
- * True when `components.css` is applied to the document, by any route. Reads the
63
- * sentinel custom property the stylesheet declares on `:root`.
78
+ * True when `components.css` is applied to the document, by any route.
79
+ *
80
+ * Two probes, because neither alone covers both deliveries. The computed
81
+ * sentinel on `<html>` is authoritative for a stylesheet applying to the whole
82
+ * document. A host that confines its copy to its player subtree — `@scope
83
+ * (.item-content) { … }`, the documented remedy for these rules reaching host
84
+ * chrome — puts the stylesheet's `:root` rule somewhere it can never match,
85
+ * since `<html>` is not a descendant of the scoping root; the property then
86
+ * reads empty while the stylesheet is present and working. So an empty read
87
+ * falls back to scanning the document's sheets for the sentinel.
64
88
  *
65
89
  * Only meaningful once the document's stylesheets have been applied — a host
66
90
  * that loads CSS via an async `<link>` reads as missing until it lands.
@@ -71,7 +95,9 @@ export function contentStylesPresent() {
71
95
  const value = getComputedStyle(document.documentElement)
72
96
  .getPropertyValue(SENTINEL_PROPERTY)
73
97
  .trim();
74
- return value !== "";
98
+ if (value !== "")
99
+ return true;
100
+ return countContentStyleSheets({ excludeInstalled: false }) > 0;
75
101
  }
76
102
  /**
77
103
  * Installs `cssText` as a document-level stylesheet, once per document.
@@ -109,21 +135,22 @@ export function installContentStyles(cssText, source) {
109
135
  return "installed";
110
136
  }
111
137
  /**
112
- * Counts content stylesheets in the document that this module did not install —
113
- * i.e. copies the host loaded itself. Detected by the sentinel property rather
114
- * than by URL, so a copy arriving as a `<link>`, a bundler-injected `<style>`, or
115
- * anything else all count the same.
138
+ * Counts content stylesheets in the document, detected by the sentinel property
139
+ * rather than by URL, so a copy arriving as a `<link>`, a bundler-injected
140
+ * `<style>`, or anything else all count the same. `excludeInstalled` narrows the
141
+ * count to copies the host loaded itself.
116
142
  *
117
143
  * Cross-origin sheets throw on `cssRules` access and are skipped; a host copy
118
144
  * served from another origin therefore reads as absent. That only costs a
119
145
  * diagnostic, never correctness.
120
146
  */
121
- const countHostContentStyleSheets = () => {
147
+ const countContentStyleSheets = ({ excludeInstalled, }) => {
122
148
  // Walks the owning elements rather than document.styleSheets: the marker
123
149
  // attribute lives on the element, and CSSStyleSheet.ownerNode is not
124
150
  // universally implemented (happy-dom omits it), which would make our own
125
151
  // installed copy look like a host copy.
126
- const nodes = document.querySelectorAll(`style:not([${MARKER_ATTRIBUTE}]), link[rel~="stylesheet"]:not([${MARKER_ATTRIBUTE}])`);
152
+ const exclusion = excludeInstalled ? `:not([${MARKER_ATTRIBUTE}])` : "";
153
+ const nodes = document.querySelectorAll(`style${exclusion}, link[rel~="stylesheet"]${exclusion}`);
127
154
  let count = 0;
128
155
  for (const node of Array.from(nodes)) {
129
156
  let rules;
@@ -144,6 +171,8 @@ const countHostContentStyleSheets = () => {
144
171
  }
145
172
  return count;
146
173
  };
174
+ /** Copies the host loaded itself, i.e. not the one this module installed. */
175
+ const countHostContentStyleSheets = () => countContentStyleSheets({ excludeInstalled: true });
147
176
  let auditWarningIssued = false;
148
177
  const pendingChecks = [];
149
178
  /**
@@ -8,3 +8,25 @@ export declare const FOCUSABLE_SELECTOR: string;
8
8
  * not disabled, not inside `inert`, and has a layout box).
9
9
  */
10
10
  export declare function isProgrammaticFocusTarget(el: HTMLElement): boolean;
11
+ /**
12
+ * Whether `el` is in the sequential tab order.
13
+ *
14
+ * Stricter than {@link isProgrammaticFocusTarget}, which deliberately accepts
15
+ * `tabindex="-1"`: such an element is a valid `focus()` target — a roving-tabindex
16
+ * control or a landmark focused after a view change — but is explicitly not tabbable.
17
+ * A tab order that included it would stop on elements the browser skips.
18
+ */
19
+ export declare function isTabbable(el: HTMLElement): boolean;
20
+ /**
21
+ * Focusable descendants of `root` in tab order, descending into open shadow roots.
22
+ *
23
+ * `querySelectorAll` stops at a shadow boundary, which makes it the wrong tool for
24
+ * collecting a tab order in this codebase: every tool renders into `shadow: "open"`,
25
+ * so a container holding one has focusable content the selector cannot see. A focus
26
+ * trap built on the flat query traps Tab in the host's own chrome and never reaches
27
+ * the tool — the tool's controls are then unreachable by keyboard entirely.
28
+ *
29
+ * Closed shadow roots are invisible to script and are skipped; nothing can be done
30
+ * for them from outside.
31
+ */
32
+ export declare function collectFocusable(root: Element | ShadowRoot): HTMLElement[];
@@ -24,3 +24,50 @@ export function isProgrammaticFocusTarget(el) {
24
24
  return false;
25
25
  return el.offsetParent !== null || el.getClientRects().length > 0;
26
26
  }
27
+ /**
28
+ * Whether `el` is in the sequential tab order.
29
+ *
30
+ * Stricter than {@link isProgrammaticFocusTarget}, which deliberately accepts
31
+ * `tabindex="-1"`: such an element is a valid `focus()` target — a roving-tabindex
32
+ * control or a landmark focused after a view change — but is explicitly not tabbable.
33
+ * A tab order that included it would stop on elements the browser skips.
34
+ */
35
+ export function isTabbable(el) {
36
+ if (el.getAttribute("tabindex") === "-1")
37
+ return false;
38
+ return isProgrammaticFocusTarget(el);
39
+ }
40
+ /**
41
+ * Focusable descendants of `root` in tab order, descending into open shadow roots.
42
+ *
43
+ * `querySelectorAll` stops at a shadow boundary, which makes it the wrong tool for
44
+ * collecting a tab order in this codebase: every tool renders into `shadow: "open"`,
45
+ * so a container holding one has focusable content the selector cannot see. A focus
46
+ * trap built on the flat query traps Tab in the host's own chrome and never reaches
47
+ * the tool — the tool's controls are then unreachable by keyboard entirely.
48
+ *
49
+ * Closed shadow roots are invisible to script and are skipped; nothing can be done
50
+ * for them from outside.
51
+ */
52
+ export function collectFocusable(root) {
53
+ const found = [];
54
+ const visit = (node) => {
55
+ for (const child of Array.from(node.children)) {
56
+ if (!(child instanceof HTMLElement))
57
+ continue;
58
+ // `inert` and its subtree take no focus, so there is nothing below to collect.
59
+ if (child.hasAttribute("inert"))
60
+ continue;
61
+ if (isTabbable(child))
62
+ found.push(child);
63
+ // A shadow host's own light children are slotted into its shadow tree, so the
64
+ // shadow root is where tab order continues.
65
+ if (child.shadowRoot)
66
+ visit(child.shadowRoot);
67
+ else
68
+ visit(child);
69
+ }
70
+ };
71
+ visit(root);
72
+ return found;
73
+ }
@@ -1,6 +1,11 @@
1
- import { FOCUSABLE_SELECTOR, isProgrammaticFocusTarget, } from "./first-focusable.js";
1
+ import { collectFocusable } from "./first-focusable.js";
2
+ /**
3
+ * Shadow-aware: a trapped panel almost always hosts a custom element rendering into
4
+ * `shadow: "open"`, and a flat `querySelectorAll` would collect only the panel's own
5
+ * chrome — trapping Tab there and leaving the hosted tool's controls unreachable.
6
+ */
2
7
  function getFocusableElements(container) {
3
- return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => isProgrammaticFocusTarget(el));
8
+ return collectFocusable(container);
4
9
  }
5
10
  function getDeepActiveElement(root) {
6
11
  let active = root.activeElement;
@@ -0,0 +1,30 @@
1
+ export type PointerDragPosition = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+ export interface PointerDragControllerArgs {
6
+ getPosition: () => PointerDragPosition;
7
+ setPosition: (next: PointerDragPosition) => void;
8
+ /** Called once a drag starts, before the first move — e.g. to bring the element to front. */
9
+ onDragStart?: (container: Element) => void;
10
+ }
11
+ export interface PointerDragController {
12
+ isDragging: () => boolean;
13
+ /**
14
+ * Begin tracking a drag from a pointerdown. Callers own pointer-capture
15
+ * release and event-listener attach/detach — this only tracks the
16
+ * position math, since a caller may also need the same pointer-capture
17
+ * session for an unrelated interaction (e.g. a resize handle).
18
+ */
19
+ startDragging: (event: PointerEvent, container: Element) => void;
20
+ /** No-ops when a drag isn't in progress, so callers can route every pointermove through this unconditionally. */
21
+ handlePointerMove: (event: PointerEvent) => void;
22
+ endDragging: () => void;
23
+ }
24
+ /**
25
+ * Tracks a pointer-capture-based drag: the position delta between where the
26
+ * pointer went down (relative to the element's current position) and where
27
+ * it currently is. Shared by every floating tool overlay that repositions
28
+ * itself by dragging its own chrome.
29
+ */
30
+ export declare function createPointerDragController(args: PointerDragControllerArgs): PointerDragController;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Tracks a pointer-capture-based drag: the position delta between where the
3
+ * pointer went down (relative to the element's current position) and where
4
+ * it currently is. Shared by every floating tool overlay that repositions
5
+ * itself by dragging its own chrome.
6
+ */
7
+ export function createPointerDragController(args) {
8
+ let dragging = false;
9
+ let dragStart = { x: 0, y: 0 };
10
+ return {
11
+ isDragging: () => dragging,
12
+ startDragging(event, container) {
13
+ container.setPointerCapture(event.pointerId);
14
+ dragging = true;
15
+ const position = args.getPosition();
16
+ dragStart = {
17
+ x: event.clientX - position.x,
18
+ y: event.clientY - position.y,
19
+ };
20
+ args.onDragStart?.(container);
21
+ },
22
+ handlePointerMove(event) {
23
+ if (!dragging)
24
+ return;
25
+ args.setPosition({
26
+ x: event.clientX - dragStart.x,
27
+ y: event.clientY - dragStart.y,
28
+ });
29
+ },
30
+ endDragging() {
31
+ dragging = false;
32
+ },
33
+ };
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-players-shared",
3
- "version": "0.3.66",
3
+ "version": "0.3.68",
4
4
  "type": "module",
5
5
  "description": "Shared runtime + UI utilities for PIE players",
6
6
  "license": "MIT",
@@ -37,6 +37,14 @@
37
37
  "types": "./dist/types/index.d.ts",
38
38
  "import": "./dist/types/index.js"
39
39
  },
40
+ "./formative": {
41
+ "types": "./dist/formative/index.d.ts",
42
+ "import": "./dist/formative/index.js"
43
+ },
44
+ "./timed-media": {
45
+ "types": "./dist/timed-media/index.d.ts",
46
+ "import": "./dist/timed-media/index.js"
47
+ },
40
48
  "./pie": {
41
49
  "types": "./dist/pie/index.d.ts",
42
50
  "import": "./dist/pie/index.js"
@@ -53,10 +61,30 @@
53
61
  "types": "./dist/server/npm-registry.d.ts",
54
62
  "import": "./dist/server/npm-registry.js"
55
63
  },
64
+ "./server/npm-auth-env": {
65
+ "types": "./dist/server/npm-auth-env.d.ts",
66
+ "import": "./dist/server/npm-auth-env.js"
67
+ },
56
68
  "./i18n": {
57
69
  "types": "./dist/i18n/index.d.ts",
58
70
  "import": "./dist/i18n/index.js"
59
71
  },
72
+ "./i18n/types": {
73
+ "types": "./dist/i18n/types.d.ts",
74
+ "import": "./dist/i18n/types.js"
75
+ },
76
+ "./i18n/provider": {
77
+ "types": "./dist/i18n/provider.d.ts",
78
+ "import": "./dist/i18n/provider.js"
79
+ },
80
+ "./i18n/catalogs": {
81
+ "types": "./dist/i18n/catalogs.d.ts",
82
+ "import": "./dist/i18n/catalogs.js"
83
+ },
84
+ "./i18n/language-tags": {
85
+ "types": "./dist/i18n/language-tags.d.ts",
86
+ "import": "./dist/i18n/language-tags.js"
87
+ },
60
88
  "./nds-icon-button": {
61
89
  "types": "./dist/components/vendor/nds/nds-icon-button.d.ts",
62
90
  "import": "./dist/components/vendor/nds/nds-icon-button.js"
@@ -68,6 +96,10 @@
68
96
  "./ui/content-styles": {
69
97
  "types": "./dist/ui/content-styles.d.ts",
70
98
  "import": "./dist/ui/content-styles.js"
99
+ },
100
+ "./tools/term-lookup": {
101
+ "types": "./dist/tools/term-lookup.d.ts",
102
+ "import": "./dist/tools/term-lookup.js"
71
103
  }
72
104
  },
73
105
  "files": [
@@ -89,11 +121,11 @@
89
121
  "scan-hardcoded": "bun run src/i18n/scripts/scan-hardcoded.ts"
90
122
  },
91
123
  "devDependencies": {
92
- "@biomejs/biome": "^2.5.7",
124
+ "@biomejs/biome": "^2.5.8",
93
125
  "@happy-dom/global-registrator": "^20.11.1",
94
126
  "@playwright/test": "^1.62.1",
95
127
  "@types/semver": "^7.8.0",
96
- "esbuild": "^0.28.1",
128
+ "esbuild": "^0.28.2",
97
129
  "glob": "^13.0.0",
98
130
  "svelte": "^5.56.8",
99
131
  "typescript": "^5.9.3"
@@ -1,35 +0,0 @@
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
- import type { TranslationBundle } from "./types.js";
11
- /**
12
- * Bundled English translations
13
- * Available immediately without network request
14
- */
15
- export declare const BUNDLED_TRANSLATIONS: Record<string, TranslationBundle>;
16
- /**
17
- * Load translations for a locale
18
- *
19
- * @param locale Locale code (e.g., 'en', 'es', 'zh', 'ar')
20
- * @returns Translation bundle with flattened keys
21
- */
22
- export declare function loadTranslations(locale: string): Promise<TranslationBundle>;
23
- /**
24
- * Get available locales
25
- *
26
- * @returns Array of locale codes
27
- */
28
- export declare function getAvailableLocales(): string[];
29
- /**
30
- * Check if locale is RTL
31
- *
32
- * @param locale Locale code
33
- * @returns True if locale is right-to-left
34
- */
35
- export declare function isRTLLocale(locale: string): boolean;
@@ -1,132 +0,0 @@
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
- // Import English translations (bundled)
11
- import enCommon from "./translations/en/common.json" with { type: "json" };
12
- import enToolkit from "./translations/en/toolkit.json" with { type: "json" };
13
- import enTools from "./translations/en/tools.json" with { type: "json" };
14
- // RTL locales
15
- const RTL_LOCALES = ["ar", "he", "fa", "ur"];
16
- /**
17
- * Bundled English translations
18
- * Available immediately without network request
19
- */
20
- export const BUNDLED_TRANSLATIONS = {
21
- en: {
22
- locale: "en",
23
- direction: "ltr",
24
- translations: {
25
- ...flattenTranslations(enCommon),
26
- ...flattenTranslations(enToolkit),
27
- ...flattenTranslations(enTools),
28
- },
29
- },
30
- };
31
- const LOCALE_IMPORTS = {
32
- es: () => Promise.all([
33
- import("./translations/es/common.json"),
34
- import("./translations/es/toolkit.json"),
35
- import("./translations/es/tools.json"),
36
- ]),
37
- zh: () => Promise.all([
38
- import("./translations/zh/common.json"),
39
- import("./translations/zh/toolkit.json"),
40
- import("./translations/zh/tools.json"),
41
- ]),
42
- ar: () => Promise.all([
43
- import("./translations/ar/common.json"),
44
- import("./translations/ar/toolkit.json"),
45
- import("./translations/ar/tools.json"),
46
- ]),
47
- };
48
- /**
49
- * Load translations for a locale
50
- *
51
- * @param locale Locale code (e.g., 'en', 'es', 'zh', 'ar')
52
- * @returns Translation bundle with flattened keys
53
- */
54
- export async function loadTranslations(locale) {
55
- // Return bundled English immediately
56
- if (locale === "en") {
57
- return BUNDLED_TRANSLATIONS.en;
58
- }
59
- const importer = LOCALE_IMPORTS[locale];
60
- if (!importer) {
61
- throw new Error(`Translation files not found for locale: ${locale}. Ensure translation files exist in packages/players-shared/src/i18n/translations/${locale}/`);
62
- }
63
- // Lazy load other locales
64
- try {
65
- const [common, toolkit, tools] = await importer();
66
- return {
67
- locale,
68
- direction: RTL_LOCALES.includes(locale) ? "rtl" : "ltr",
69
- translations: {
70
- ...flattenTranslations(common.default || common),
71
- ...flattenTranslations(toolkit.default || toolkit),
72
- ...flattenTranslations(tools.default || tools),
73
- },
74
- };
75
- }
76
- catch (error) {
77
- console.error(`Failed to load translations for locale: ${locale}`, error);
78
- throw new Error(`Translation files not found for locale: ${locale}. Ensure translation files exist in packages/players-shared/src/i18n/translations/${locale}/`);
79
- }
80
- }
81
- /**
82
- * Flatten nested JSON to dot notation
83
- *
84
- * Converts:
85
- * { "common": { "save": "Save" } }
86
- *
87
- * To:
88
- * { "common.save": "Save" }
89
- *
90
- * @param obj Nested translation object
91
- * @param prefix Current key prefix
92
- * @param result Accumulated result
93
- * @returns Flattened translation object
94
- */
95
- function flattenTranslations(obj, prefix = "", result = {}) {
96
- for (const [key, value] of Object.entries(obj)) {
97
- const newKey = prefix ? `${prefix}.${key}` : key;
98
- if (value && typeof value === "object" && !Array.isArray(value)) {
99
- // Check if it's a plural form (has 'one' or 'other' keys)
100
- if ("one" in value || "other" in value) {
101
- // This is a plural translation, keep it as an object
102
- result[newKey] = value;
103
- }
104
- else {
105
- // This is a nested object, flatten recursively
106
- flattenTranslations(value, newKey, result);
107
- }
108
- }
109
- else {
110
- // This is a leaf value
111
- result[newKey] = value;
112
- }
113
- }
114
- return result;
115
- }
116
- /**
117
- * Get available locales
118
- *
119
- * @returns Array of locale codes
120
- */
121
- export function getAvailableLocales() {
122
- return ["en", "es", "zh", "ar"];
123
- }
124
- /**
125
- * Check if locale is RTL
126
- *
127
- * @param locale Locale code
128
- * @returns True if locale is right-to-left
129
- */
130
- export function isRTLLocale(locale) {
131
- return RTL_LOCALES.includes(locale);
132
- }
@@ -1,68 +0,0 @@
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
- import type { I18nConfig } from "./types.js";
8
- /**
9
- * Simple I18n class for standalone use
10
- */
11
- export declare class SimpleI18n {
12
- private locale;
13
- private fallbackLocale;
14
- private direction;
15
- private translations;
16
- private listeners;
17
- private loadingPromises;
18
- private config;
19
- constructor(config?: I18nConfig);
20
- /**
21
- * Initialize with locale
22
- */
23
- initialize(config: I18nConfig): Promise<void>;
24
- /**
25
- * Translate a key
26
- */
27
- t(key: string, params?: Record<string, any>): string;
28
- /**
29
- * Translate with pluralization
30
- */
31
- tn(key: string, count: number, params?: Record<string, any>): string;
32
- /**
33
- * Get current locale
34
- */
35
- getLocale(): string;
36
- /**
37
- * Change locale
38
- */
39
- setLocale(locale: string): Promise<void>;
40
- /**
41
- * Get current direction
42
- */
43
- getDirection(): "ltr" | "rtl";
44
- /**
45
- * Get available locales
46
- */
47
- getAvailableLocales(): string[];
48
- /**
49
- * Check if locale is loaded
50
- */
51
- isLocaleLoaded(locale: string): boolean;
52
- /**
53
- * Subscribe to changes
54
- */
55
- subscribe(listener: () => void): () => void;
56
- /**
57
- * Check if key exists
58
- */
59
- hasKey(key: string): boolean;
60
- private notifyListeners;
61
- private applyLocale;
62
- private applyDOMDirection;
63
- private loadTranslationsForLocale;
64
- private getTranslation;
65
- private interpolate;
66
- private selectPluralForm;
67
- private detectBrowserLocale;
68
- }