@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.
- package/README.md +59 -0
- package/dist/formative/index.d.ts +16 -0
- package/dist/formative/index.js +5 -0
- package/dist/formative/mastery.d.ts +16 -0
- package/dist/formative/mastery.js +43 -0
- package/dist/formative/outcome.d.ts +19 -0
- package/dist/formative/outcome.js +90 -0
- package/dist/formative/policy.d.ts +37 -0
- package/dist/formative/policy.js +86 -0
- package/dist/formative/session.d.ts +19 -0
- package/dist/formative/session.js +113 -0
- package/dist/formative/state.d.ts +60 -0
- package/dist/formative/state.js +140 -0
- package/dist/formative/types.d.ts +164 -0
- package/dist/formative/types.js +13 -0
- package/dist/i18n/catalogs.d.ts +38 -0
- package/dist/i18n/catalogs.js +47 -0
- package/dist/i18n/index.d.ts +28 -4
- package/dist/i18n/index.js +34 -3
- package/dist/i18n/language-tags.d.ts +51 -0
- package/dist/i18n/language-tags.js +84 -0
- package/dist/i18n/messages/en-US.d.ts +601 -0
- package/dist/i18n/messages/en-US.js +617 -0
- package/dist/i18n/messages/nl-NL.d.ts +516 -0
- package/dist/i18n/messages/nl-NL.js +516 -0
- package/dist/i18n/provider.d.ts +168 -0
- package/dist/i18n/provider.js +472 -0
- package/dist/i18n/types.d.ts +177 -28
- package/dist/i18n/types.js +6 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/pie/custom-element-define.d.ts +24 -0
- package/dist/pie/custom-element-define.js +36 -6
- package/dist/pie/index.d.ts +1 -1
- package/dist/pie/index.js +1 -1
- package/dist/pie/versioned-tag.js +3 -2
- package/dist/security/sanitize-forbidden-lists.d.ts +8 -0
- package/dist/security/sanitize-forbidden-lists.js +41 -0
- package/dist/security/sanitize-item-markup.js +3 -36
- package/dist/security/sanitize-svg-icon.js +3 -25
- package/dist/security/wrap-overwide-images.d.ts +5 -10
- package/dist/security/wrap-overwide-images.js +13 -62
- package/dist/security/wrap-overwide-tables.d.ts +5 -10
- package/dist/security/wrap-overwide-tables.js +13 -62
- package/dist/security/wrap-overwide.d.ts +51 -0
- package/dist/security/wrap-overwide.js +91 -0
- package/dist/server/npm-auth-env.d.ts +13 -0
- package/dist/server/npm-auth-env.js +64 -0
- package/dist/timed-media/index.d.ts +17 -0
- package/dist/timed-media/index.js +16 -0
- package/dist/timed-media/media-element-source.d.ts +36 -0
- package/dist/timed-media/media-element-source.js +121 -0
- package/dist/timed-media/policy.d.ts +36 -0
- package/dist/timed-media/policy.js +297 -0
- package/dist/timed-media/session.d.ts +21 -0
- package/dist/timed-media/session.js +85 -0
- package/dist/timed-media/state.d.ts +66 -0
- package/dist/timed-media/state.js +357 -0
- package/dist/timed-media/types.d.ts +343 -0
- package/dist/timed-media/types.js +12 -0
- package/dist/tools/term-lookup.d.ts +189 -0
- package/dist/tools/term-lookup.js +284 -0
- package/dist/types/index.d.ts +105 -1
- package/dist/ui/attribute-coercion.d.ts +1 -0
- package/dist/ui/attribute-coercion.js +19 -0
- package/dist/ui/first-focusable.d.ts +22 -0
- package/dist/ui/first-focusable.js +47 -0
- package/dist/ui/focus-trap.js +7 -2
- package/dist/ui/pointer-drag.d.ts +30 -0
- package/dist/ui/pointer-drag.js +34 -0
- package/package.json +38 -6
- package/dist/i18n/loader.d.ts +0 -35
- package/dist/i18n/loader.js +0 -132
- package/dist/i18n/simple-i18n.d.ts +0 -68
- package/dist/i18n/simple-i18n.js +0 -198
- package/dist/i18n/translations/ar/common.json +0 -36
- package/dist/i18n/translations/ar/toolkit.json +0 -48
- package/dist/i18n/translations/ar/tools.json +0 -103
- package/dist/i18n/translations/en/common.json +0 -36
- package/dist/i18n/translations/en/toolkit.json +0 -48
- package/dist/i18n/translations/en/tools.json +0 -103
- package/dist/i18n/translations/es/common.json +0 -36
- package/dist/i18n/translations/es/toolkit.json +0 -48
- package/dist/i18n/translations/es/tools.json +0 -103
- package/dist/i18n/translations/zh/common.json +0 -36
- package/dist/i18n/translations/zh/toolkit.json +0 -48
- 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
|
+
}
|
package/dist/ui/focus-trap.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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.
|
|
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": "
|
|
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.
|
|
93
|
-
"@happy-dom/global-registrator": "^20.11.
|
|
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.
|
|
128
|
+
"esbuild": "^0.28.2",
|
|
97
129
|
"glob": "^13.0.0",
|
|
98
|
-
"svelte": "^5.56.
|
|
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",
|
package/dist/i18n/loader.d.ts
DELETED
|
@@ -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;
|
package/dist/i18n/loader.js
DELETED
|
@@ -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
|
-
}
|
package/dist/i18n/simple-i18n.js
DELETED
|
@@ -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
|
-
}
|