@pie-players/pie-players-shared 0.3.67 → 0.3.69

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 (87) 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/first-focusable.d.ts +22 -0
  67. package/dist/ui/first-focusable.js +47 -0
  68. package/dist/ui/focus-trap.js +7 -2
  69. package/dist/ui/pointer-drag.d.ts +30 -0
  70. package/dist/ui/pointer-drag.js +34 -0
  71. package/package.json +38 -6
  72. package/dist/i18n/loader.d.ts +0 -35
  73. package/dist/i18n/loader.js +0 -132
  74. package/dist/i18n/simple-i18n.d.ts +0 -68
  75. package/dist/i18n/simple-i18n.js +0 -198
  76. package/dist/i18n/translations/ar/common.json +0 -36
  77. package/dist/i18n/translations/ar/toolkit.json +0 -48
  78. package/dist/i18n/translations/ar/tools.json +0 -103
  79. package/dist/i18n/translations/en/common.json +0 -36
  80. package/dist/i18n/translations/en/toolkit.json +0 -48
  81. package/dist/i18n/translations/en/tools.json +0 -103
  82. package/dist/i18n/translations/es/common.json +0 -36
  83. package/dist/i18n/translations/es/toolkit.json +0 -48
  84. package/dist/i18n/translations/es/tools.json +0 -103
  85. package/dist/i18n/translations/zh/common.json +0 -36
  86. package/dist/i18n/translations/zh/toolkit.json +0 -48
  87. package/dist/i18n/translations/zh/tools.json +0 -103
@@ -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.67",
3
+ "version": "0.3.69",
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,13 +96,17 @@
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": [
74
106
  "dist"
75
107
  ],
76
108
  "dependencies": {
77
- "@pie-lib/math-rendering-module": "5.1.2",
109
+ "@pie-lib/math-rendering-module": "6.0.0",
78
110
  "dompurify": "^3.4.13",
79
111
  "semver": "^7.8.5"
80
112
  },
@@ -89,13 +121,13 @@
89
121
  "scan-hardcoded": "bun run src/i18n/scripts/scan-hardcoded.ts"
90
122
  },
91
123
  "devDependencies": {
92
- "@biomejs/biome": "^2.5.7",
93
- "@happy-dom/global-registrator": "^20.11.1",
124
+ "@biomejs/biome": "^2.5.10",
125
+ "@happy-dom/global-registrator": "^20.11.6",
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
- "svelte": "^5.56.8",
130
+ "svelte": "^5.56.10",
99
131
  "typescript": "^5.9.3"
100
132
  },
101
133
  "homepage": "https://github.com/pie-framework/pie-players/tree/master/packages/players-shared#readme",
@@ -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
- }
@@ -1,198 +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
- /**
8
- * Simple I18n class for standalone use
9
- */
10
- export class SimpleI18n {
11
- locale = "en";
12
- fallbackLocale = "en";
13
- direction = "ltr";
14
- translations = new Map();
15
- listeners = new Set();
16
- loadingPromises = new Map();
17
- config;
18
- constructor(config = {}) {
19
- this.config = config;
20
- this.fallbackLocale = config.fallbackLocale || "en";
21
- // Load bundled translations
22
- if (config.bundledTranslations) {
23
- for (const [locale, bundle] of Object.entries(config.bundledTranslations)) {
24
- this.translations.set(locale, bundle);
25
- }
26
- }
27
- }
28
- /**
29
- * Initialize with locale
30
- */
31
- async initialize(config) {
32
- Object.assign(this.config, config);
33
- const locale = config.locale || this.detectBrowserLocale();
34
- await this.setLocale(locale);
35
- }
36
- /**
37
- * Translate a key
38
- */
39
- t(key, params) {
40
- const translation = this.getTranslation(key);
41
- return this.interpolate(translation, params);
42
- }
43
- /**
44
- * Translate with pluralization
45
- */
46
- tn(key, count, params) {
47
- const translation = this.getTranslation(key);
48
- if (typeof translation === "object") {
49
- const pluralForm = this.selectPluralForm(count, this.locale);
50
- const text = translation[pluralForm] || translation.other;
51
- return this.interpolate(text, { ...params, count });
52
- }
53
- return this.interpolate(translation, { ...params, count });
54
- }
55
- /**
56
- * Get current locale
57
- */
58
- getLocale() {
59
- return this.locale;
60
- }
61
- /**
62
- * Change locale
63
- */
64
- async setLocale(locale) {
65
- // Check if already loading
66
- if (this.loadingPromises.has(locale)) {
67
- await this.loadingPromises.get(locale);
68
- return;
69
- }
70
- // Check if already loaded
71
- if (this.translations.has(locale)) {
72
- this.applyLocale(locale);
73
- return;
74
- }
75
- // Load translations
76
- const loadingPromise = this.loadTranslationsForLocale(locale);
77
- this.loadingPromises.set(locale, loadingPromise);
78
- try {
79
- await loadingPromise;
80
- this.applyLocale(locale);
81
- }
82
- finally {
83
- this.loadingPromises.delete(locale);
84
- }
85
- }
86
- /**
87
- * Get current direction
88
- */
89
- getDirection() {
90
- return this.direction;
91
- }
92
- /**
93
- * Get available locales
94
- */
95
- getAvailableLocales() {
96
- return Array.from(this.translations.keys());
97
- }
98
- /**
99
- * Check if locale is loaded
100
- */
101
- isLocaleLoaded(locale) {
102
- return this.translations.has(locale);
103
- }
104
- /**
105
- * Subscribe to changes
106
- */
107
- subscribe(listener) {
108
- this.listeners.add(listener);
109
- return () => {
110
- this.listeners.delete(listener);
111
- };
112
- }
113
- /**
114
- * Check if key exists
115
- */
116
- hasKey(key) {
117
- const bundle = this.translations.get(this.locale);
118
- return !!bundle?.translations[key];
119
- }
120
- notifyListeners() {
121
- for (const listener of this.listeners) {
122
- listener();
123
- }
124
- }
125
- applyLocale(locale) {
126
- const bundle = this.translations.get(locale);
127
- if (!bundle)
128
- return;
129
- this.locale = locale;
130
- this.direction = bundle.direction;
131
- this.applyDOMDirection();
132
- this.notifyListeners();
133
- }
134
- applyDOMDirection() {
135
- if (typeof document === "undefined")
136
- return;
137
- document.documentElement.setAttribute("dir", this.direction);
138
- document.documentElement.setAttribute("lang", this.locale);
139
- }
140
- async loadTranslationsForLocale(locale) {
141
- if (!this.config.loadTranslations) {
142
- throw new Error(`No translation loader configured for locale: ${locale}`);
143
- }
144
- const bundle = await this.config.loadTranslations(locale);
145
- this.translations.set(locale, bundle);
146
- }
147
- getTranslation(key) {
148
- // Try current locale
149
- const currentBundle = this.translations.get(this.locale);
150
- if (currentBundle?.translations[key]) {
151
- return currentBundle.translations[key];
152
- }
153
- // Try fallback locale
154
- const fallbackBundle = this.translations.get(this.fallbackLocale);
155
- if (fallbackBundle?.translations[key]) {
156
- return fallbackBundle.translations[key];
157
- }
158
- // Missing key
159
- if (this.config.onMissingKey) {
160
- this.config.onMissingKey(key, this.locale);
161
- }
162
- return key;
163
- }
164
- interpolate(text, params) {
165
- if (!params)
166
- return text;
167
- return text.replace(/\{(\w+)\}/g, (match, key) => {
168
- return params[key]?.toString() || match;
169
- });
170
- }
171
- selectPluralForm(count, locale) {
172
- // Use Intl.PluralRules if available
173
- if (typeof Intl !== "undefined" && Intl.PluralRules) {
174
- try {
175
- const rules = new Intl.PluralRules(locale);
176
- const category = rules.select(count);
177
- return category;
178
- }
179
- catch {
180
- // Fall through to simple rules
181
- }
182
- }
183
- // Fallback to simple rules
184
- if (count === 0)
185
- return "zero";
186
- if (count === 1)
187
- return "one";
188
- return "other";
189
- }
190
- detectBrowserLocale() {
191
- if (typeof navigator === "undefined")
192
- return "en";
193
- const browserLang = navigator.language ||
194
- (navigator.languages && navigator.languages[0]) ||
195
- "en";
196
- return browserLang.split("-")[0];
197
- }
198
- }
@@ -1,36 +0,0 @@
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
- }