@pie-players/pie-assessment-toolkit 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.
- package/README.md +1 -1
- package/dist/attempt/AssessmentSession.d.ts +7 -27
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +13 -13
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-38mhtjsq.js +51 -0
- package/dist/components/chunks/ItemToolBar-9ymm7pd1.js +46 -0
- package/dist/components/item-toolbar-element.js +1 -0
- package/dist/components/pie-assessment-toolkit-element.js +1 -0
- package/dist/components/section-toolbar-element.js +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +28 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +12 -0
- package/dist/policy/core/ToolPolicyEngine.js +28 -2
- package/dist/policy/core/feature-decision.d.ts +28 -2
- package/dist/policy/core/feature-decision.js +35 -0
- package/dist/policy/engine.d.ts +1 -1
- package/dist/policy/engine.js +1 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +56 -0
- package/dist/runtime/SectionRuntimeEngine.js +66 -1
- package/dist/runtime/core/engine-resolver.d.ts +25 -1
- package/dist/runtime/registration-events.d.ts +52 -0
- package/dist/runtime/registration-events.js +2 -0
- package/dist/services/AccessibilityCatalogResolver.js +21 -5
- package/dist/services/I18nService.d.ts +28 -100
- package/dist/services/I18nService.js +43 -233
- package/dist/services/TTSService.d.ts +12 -0
- package/dist/services/TTSService.js +18 -17
- package/dist/services/ToolRegistry.d.ts +86 -2
- package/dist/services/ToolRegistry.js +30 -0
- package/dist/services/ToolkitCoordinator.d.ts +39 -37
- package/dist/services/ToolkitCoordinator.js +40 -0
- package/dist/services/audio-handoff.d.ts +39 -0
- package/dist/services/audio-handoff.js +58 -0
- package/dist/services/catalog-media.d.ts +35 -4
- package/dist/services/catalog-media.js +92 -3
- package/dist/services/framework-error.d.ts +15 -1
- package/dist/services/interfaces.d.ts +28 -0
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/section-controller-types.d.ts +218 -7
- package/dist/services/selection-action.d.ts +49 -0
- package/dist/services/selection-action.js +10 -0
- package/dist/services/spoken-audio-cards.js +5 -1
- package/dist/services/tool-context.d.ts +6 -5
- package/dist/services/tool-context.js +197 -152
- package/dist/services/tool-icons.d.ts +18 -0
- package/dist/services/tool-icons.js +31 -0
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +6 -5
- package/dist/services/tool-request.d.ts +106 -0
- package/dist/services/tool-request.js +127 -0
- package/dist/services/toolbar-items.d.ts +6 -0
- package/dist/tools/client.d.ts +0 -1
- package/dist/tools/client.js +0 -2
- package/dist/tools/content-capability-resolution.d.ts +106 -0
- package/dist/tools/content-capability-resolution.js +136 -0
- package/dist/tools/internal.d.ts +8 -0
- package/dist/tools/internal.js +8 -0
- package/dist/tools/tool-surface-host.d.ts +57 -0
- package/dist/tools/tool-surface-host.js +610 -0
- package/package.json +14 -10
- package/dist/components/chunks/ItemToolBar-8jgdz50p.js +0 -51
- package/dist/components/chunks/ItemToolBar-cvs646j3.js +0 -36
- package/dist/tools/calculators/desmos-provider.d.ts +0 -46
- package/dist/tools/calculators/desmos-provider.js +0 -393
|
@@ -1,119 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* I18nService
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The toolkit's view of the shared i18n provider: a delegating wrapper over
|
|
5
|
+
* `SimpleI18n` from `players-shared` that adds toolkit-scoped logging and wires
|
|
6
|
+
* the lazy catalog loaders.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - Pluralization: ICU-style plural rules
|
|
12
|
-
* - RTL detection: Automatic based on locale
|
|
13
|
-
* - Fallback to English for missing keys
|
|
8
|
+
* It was once a near-verbatim second copy of that class — same fields, same
|
|
9
|
+
* lookup, same plural selection. Duplicate implementations of one contract drift
|
|
10
|
+
* silently, because nothing fails when only one of them is fixed. Keep this a
|
|
11
|
+
* wrapper.
|
|
14
12
|
*
|
|
15
13
|
* Part of PIE Assessment Toolkit.
|
|
16
14
|
*/
|
|
17
|
-
import type { I18nConfig, I18nServiceApi,
|
|
18
|
-
export type { I18nConfig, I18nServiceApi,
|
|
15
|
+
import type { I18nConfig, I18nProvider, I18nServiceApi, InterpolationValues, LocaleCode, MessageCatalog, MessageKeyInput, PluralOptions, TextDirection } from "@pie-players/pie-players-shared/i18n";
|
|
16
|
+
export type { I18nConfig, I18nProvider, I18nServiceApi, InterpolationValues, LocaleCode, MessageCatalog, MessageKeyInput, PluralOptions, TextDirection, };
|
|
19
17
|
/**
|
|
20
|
-
*
|
|
18
|
+
* Toolkit-scoped i18n service.
|
|
21
19
|
*
|
|
22
|
-
*
|
|
20
|
+
* Serves every locale this repository ships, English resident and the rest
|
|
21
|
+
* lazily loaded.
|
|
23
22
|
*/
|
|
24
23
|
export declare class I18nService implements I18nServiceApi {
|
|
25
|
-
private
|
|
26
|
-
private fallbackLocale;
|
|
27
|
-
private direction;
|
|
28
|
-
private translations;
|
|
29
|
-
private listeners;
|
|
30
|
-
private loadingPromises;
|
|
31
|
-
private config;
|
|
24
|
+
private readonly i18n;
|
|
32
25
|
constructor(config?: I18nConfig);
|
|
33
26
|
/**
|
|
34
|
-
*
|
|
35
|
-
*/
|
|
36
|
-
initialize(config: I18nConfig): Promise<void>;
|
|
37
|
-
/**
|
|
38
|
-
* Translate a key with optional interpolation
|
|
39
|
-
*
|
|
40
|
-
* @param key Translation key (e.g., 'common.save')
|
|
41
|
-
* @param params Optional parameters for interpolation
|
|
42
|
-
* @returns Translated string
|
|
43
|
-
*/
|
|
44
|
-
t(key: string, params?: Record<string, any>): string;
|
|
45
|
-
/**
|
|
46
|
-
* Translate with pluralization
|
|
27
|
+
* Set the locale, loading its catalog first.
|
|
47
28
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* @returns Translated string with plural form
|
|
52
|
-
*/
|
|
53
|
-
tn(key: string, count: number, params?: Record<string, any>): string;
|
|
54
|
-
/**
|
|
55
|
-
* Get current locale
|
|
29
|
+
* Never falls back to the browser locale: a rendered-string change reaches a
|
|
30
|
+
* host's live delivery on their next install with no build signal on their
|
|
31
|
+
* side, so the locale has to be something the host asked for.
|
|
56
32
|
*/
|
|
33
|
+
initialize(config: I18nConfig): Promise<void>;
|
|
34
|
+
t(key: MessageKeyInput, values?: InterpolationValues): string;
|
|
35
|
+
plural(key: MessageKeyInput, options: PluralOptions): string;
|
|
57
36
|
getLocale(): string;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*
|
|
61
|
-
* @param locale Locale code (e.g., 'en', 'es', 'zh', 'ar')
|
|
62
|
-
*/
|
|
63
|
-
setLocale(locale: string): Promise<void>;
|
|
64
|
-
/**
|
|
65
|
-
* Get current text direction
|
|
66
|
-
*/
|
|
67
|
-
getDirection(): "ltr" | "rtl";
|
|
68
|
-
/**
|
|
69
|
-
* Get available locales
|
|
70
|
-
*/
|
|
37
|
+
setLocale(locale: LocaleCode): Promise<void>;
|
|
38
|
+
getDirection(): TextDirection;
|
|
71
39
|
getAvailableLocales(): string[];
|
|
72
|
-
|
|
73
|
-
* Check if locale is loaded
|
|
74
|
-
*/
|
|
75
|
-
isLocaleLoaded(locale: string): boolean;
|
|
76
|
-
/**
|
|
77
|
-
* Subscribe to locale/translation changes
|
|
78
|
-
* Returns unsubscribe function
|
|
79
|
-
*/
|
|
40
|
+
isLocaleLoaded(locale: LocaleCode): boolean;
|
|
80
41
|
subscribe(listener: () => void): () => void;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* Notify all listeners of state change
|
|
87
|
-
*/
|
|
88
|
-
private notifyListeners;
|
|
89
|
-
/**
|
|
90
|
-
* Apply locale and notify listeners
|
|
91
|
-
*/
|
|
92
|
-
private applyLocale;
|
|
93
|
-
/**
|
|
94
|
-
* Apply direction to DOM
|
|
95
|
-
*/
|
|
96
|
-
private applyDOMDirection;
|
|
97
|
-
/**
|
|
98
|
-
* Load translations for a locale
|
|
99
|
-
*/
|
|
100
|
-
private loadTranslationsForLocale;
|
|
101
|
-
/**
|
|
102
|
-
* Get translation for a key
|
|
103
|
-
*/
|
|
104
|
-
private getTranslation;
|
|
105
|
-
/**
|
|
106
|
-
* Interpolate variables in translation string
|
|
107
|
-
* Replaces {variable} with params.variable
|
|
108
|
-
*/
|
|
109
|
-
private interpolate;
|
|
110
|
-
/**
|
|
111
|
-
* Select plural form based on count and locale
|
|
112
|
-
* Simplified implementation - real implementation would use Intl.PluralRules
|
|
113
|
-
*/
|
|
114
|
-
private selectPluralForm;
|
|
115
|
-
/**
|
|
116
|
-
* Detect browser locale
|
|
117
|
-
*/
|
|
118
|
-
private detectBrowserLocale;
|
|
42
|
+
hasKey(key: MessageKeyInput): boolean;
|
|
43
|
+
addCustomMessages(locale: string, messages: MessageCatalog): void;
|
|
44
|
+
withLocale(locale: LocaleCode): I18nProvider;
|
|
45
|
+
formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
|
|
46
|
+
formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string;
|
|
119
47
|
}
|
|
@@ -1,274 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* I18nService
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The toolkit's view of the shared i18n provider: a delegating wrapper over
|
|
5
|
+
* `SimpleI18n` from `players-shared` that adds toolkit-scoped logging and wires
|
|
6
|
+
* the lazy catalog loaders.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - Pluralization: ICU-style plural rules
|
|
12
|
-
* - RTL detection: Automatic based on locale
|
|
13
|
-
* - Fallback to English for missing keys
|
|
8
|
+
* It was once a near-verbatim second copy of that class — same fields, same
|
|
9
|
+
* lookup, same plural selection. Duplicate implementations of one contract drift
|
|
10
|
+
* silently, because nothing fails when only one of them is fixed. Keep this a
|
|
11
|
+
* wrapper.
|
|
14
12
|
*
|
|
15
13
|
* Part of PIE Assessment Toolkit.
|
|
16
14
|
*/
|
|
15
|
+
import { createPieI18n } from "@pie-players/pie-players-shared/i18n";
|
|
17
16
|
import { createLogger } from "../utils/logger.js";
|
|
18
17
|
const log = createLogger("I18nService");
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
19
|
+
* Toolkit-scoped i18n service.
|
|
21
20
|
*
|
|
22
|
-
*
|
|
21
|
+
* Serves every locale this repository ships, English resident and the rest
|
|
22
|
+
* lazily loaded.
|
|
23
23
|
*/
|
|
24
24
|
export class I18nService {
|
|
25
|
-
|
|
26
|
-
fallbackLocale = "en";
|
|
27
|
-
direction = "ltr";
|
|
28
|
-
translations = new Map();
|
|
29
|
-
listeners = new Set();
|
|
30
|
-
loadingPromises = new Map();
|
|
31
|
-
config;
|
|
25
|
+
i18n;
|
|
32
26
|
constructor(config = {}) {
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
for (const [locale, bundle] of Object.entries(config.bundledTranslations)) {
|
|
39
|
-
this.translations.set(locale, bundle);
|
|
40
|
-
log(`Loaded bundled translations for: ${locale}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
27
|
+
this.i18n = createPieI18n(config);
|
|
28
|
+
log("I18nService created", {
|
|
29
|
+
locale: this.i18n.getLocale(),
|
|
30
|
+
fallbackLocale: config.fallbackLocale ?? "en-US",
|
|
31
|
+
});
|
|
43
32
|
}
|
|
44
33
|
/**
|
|
45
|
-
*
|
|
34
|
+
* Set the locale, loading its catalog first.
|
|
35
|
+
*
|
|
36
|
+
* Never falls back to the browser locale: a rendered-string change reaches a
|
|
37
|
+
* host's live delivery on their next install with no build signal on their
|
|
38
|
+
* side, so the locale has to be something the host asked for.
|
|
46
39
|
*/
|
|
47
40
|
async initialize(config) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
log(`Initializing with locale: ${locale}`);
|
|
51
|
-
await this.setLocale(locale);
|
|
41
|
+
log(`Initializing with locale: ${config.locale ?? this.i18n.getLocale()}`);
|
|
42
|
+
await this.i18n.initialize(config);
|
|
52
43
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
* @param key Translation key (e.g., 'common.save')
|
|
57
|
-
* @param params Optional parameters for interpolation
|
|
58
|
-
* @returns Translated string
|
|
59
|
-
*/
|
|
60
|
-
t(key, params) {
|
|
61
|
-
const translation = this.getTranslation(key);
|
|
62
|
-
return this.interpolate(translation, params);
|
|
44
|
+
t(key, values) {
|
|
45
|
+
return this.i18n.t(key, values);
|
|
63
46
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*
|
|
67
|
-
* @param key Translation key
|
|
68
|
-
* @param count Count for pluralization
|
|
69
|
-
* @param params Optional parameters for interpolation
|
|
70
|
-
* @returns Translated string with plural form
|
|
71
|
-
*/
|
|
72
|
-
tn(key, count, params) {
|
|
73
|
-
const translation = this.getTranslation(key);
|
|
74
|
-
if (typeof translation === "object") {
|
|
75
|
-
const pluralForm = this.selectPluralForm(count, this.locale);
|
|
76
|
-
const text = translation[pluralForm] || translation.other;
|
|
77
|
-
return this.interpolate(text, { ...params, count });
|
|
78
|
-
}
|
|
79
|
-
return this.interpolate(translation, { ...params, count });
|
|
47
|
+
plural(key, options) {
|
|
48
|
+
return this.i18n.plural(key, options);
|
|
80
49
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Get current locale
|
|
83
|
-
*/
|
|
84
50
|
getLocale() {
|
|
85
|
-
return this.
|
|
51
|
+
return this.i18n.getLocale();
|
|
86
52
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Change locale (triggers async loading if needed)
|
|
89
|
-
*
|
|
90
|
-
* @param locale Locale code (e.g., 'en', 'es', 'zh', 'ar')
|
|
91
|
-
*/
|
|
92
53
|
async setLocale(locale) {
|
|
93
54
|
log(`Setting locale to: ${locale}`);
|
|
94
|
-
|
|
95
|
-
if (this.loadingPromises.has(locale)) {
|
|
96
|
-
log(`Locale ${locale} is already loading, waiting...`);
|
|
97
|
-
await this.loadingPromises.get(locale);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
// Check if already loaded
|
|
101
|
-
if (this.translations.has(locale)) {
|
|
102
|
-
log(`Locale ${locale} already loaded`);
|
|
103
|
-
this.applyLocale(locale);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
// Load translations
|
|
107
|
-
const loadingPromise = this.loadTranslationsForLocale(locale);
|
|
108
|
-
this.loadingPromises.set(locale, loadingPromise);
|
|
109
|
-
try {
|
|
110
|
-
await loadingPromise;
|
|
111
|
-
this.applyLocale(locale);
|
|
112
|
-
}
|
|
113
|
-
finally {
|
|
114
|
-
this.loadingPromises.delete(locale);
|
|
115
|
-
}
|
|
55
|
+
await this.i18n.setLocale(locale);
|
|
116
56
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Get current text direction
|
|
119
|
-
*/
|
|
120
57
|
getDirection() {
|
|
121
|
-
return this.
|
|
58
|
+
return this.i18n.getDirection();
|
|
122
59
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Get available locales
|
|
125
|
-
*/
|
|
126
60
|
getAvailableLocales() {
|
|
127
|
-
return
|
|
61
|
+
return this.i18n.getAvailableLocales();
|
|
128
62
|
}
|
|
129
|
-
/**
|
|
130
|
-
* Check if locale is loaded
|
|
131
|
-
*/
|
|
132
63
|
isLocaleLoaded(locale) {
|
|
133
|
-
return this.
|
|
64
|
+
return this.i18n.isLocaleLoaded(locale);
|
|
134
65
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Subscribe to locale/translation changes
|
|
137
|
-
* Returns unsubscribe function
|
|
138
|
-
*/
|
|
139
66
|
subscribe(listener) {
|
|
140
|
-
this.
|
|
141
|
-
return () => {
|
|
142
|
-
this.listeners.delete(listener);
|
|
143
|
-
};
|
|
67
|
+
return this.i18n.subscribe(listener);
|
|
144
68
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Check if a translation key exists
|
|
147
|
-
*/
|
|
148
69
|
hasKey(key) {
|
|
149
|
-
|
|
150
|
-
return !!bundle?.translations[key];
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Notify all listeners of state change
|
|
154
|
-
*/
|
|
155
|
-
notifyListeners() {
|
|
156
|
-
this.listeners.forEach((listener) => listener());
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Apply locale and notify listeners
|
|
160
|
-
*/
|
|
161
|
-
applyLocale(locale) {
|
|
162
|
-
const bundle = this.translations.get(locale);
|
|
163
|
-
if (!bundle) {
|
|
164
|
-
log(`No bundle found for locale: ${locale}`, "warn");
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const previousLocale = this.locale;
|
|
168
|
-
this.locale = locale;
|
|
169
|
-
this.direction = bundle.direction;
|
|
170
|
-
log(`Applied locale: ${locale} (direction: ${this.direction})`);
|
|
171
|
-
this.applyDOMDirection();
|
|
172
|
-
this.notifyListeners();
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Apply direction to DOM
|
|
176
|
-
*/
|
|
177
|
-
applyDOMDirection() {
|
|
178
|
-
if (typeof document === "undefined")
|
|
179
|
-
return;
|
|
180
|
-
document.documentElement.setAttribute("dir", this.direction);
|
|
181
|
-
document.documentElement.setAttribute("lang", this.locale);
|
|
182
|
-
log(`Applied DOM attributes: dir="${this.direction}" lang="${this.locale}"`);
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Load translations for a locale
|
|
186
|
-
*/
|
|
187
|
-
async loadTranslationsForLocale(locale) {
|
|
188
|
-
if (!this.config.loadTranslations) {
|
|
189
|
-
throw new Error(`No translation loader configured for locale: ${locale}`);
|
|
190
|
-
}
|
|
191
|
-
log(`Loading translations for: ${locale}`);
|
|
192
|
-
try {
|
|
193
|
-
const bundle = await this.config.loadTranslations(locale);
|
|
194
|
-
this.translations.set(locale, bundle);
|
|
195
|
-
log(`Loaded translations for: ${locale} (${Object.keys(bundle.translations).length} keys)`);
|
|
196
|
-
}
|
|
197
|
-
catch (error) {
|
|
198
|
-
log(`Failed to load translations for: ${locale}`, "error");
|
|
199
|
-
throw error;
|
|
200
|
-
}
|
|
70
|
+
return this.i18n.hasKey(key);
|
|
201
71
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*/
|
|
205
|
-
getTranslation(key) {
|
|
206
|
-
// Try current locale
|
|
207
|
-
const currentBundle = this.translations.get(this.locale);
|
|
208
|
-
if (currentBundle?.translations[key]) {
|
|
209
|
-
return currentBundle.translations[key];
|
|
210
|
-
}
|
|
211
|
-
// Try fallback locale
|
|
212
|
-
const fallbackBundle = this.translations.get(this.fallbackLocale);
|
|
213
|
-
if (fallbackBundle?.translations[key]) {
|
|
214
|
-
if (this.config.debug) {
|
|
215
|
-
log(`Using fallback translation for key: ${key}`);
|
|
216
|
-
}
|
|
217
|
-
return fallbackBundle.translations[key];
|
|
218
|
-
}
|
|
219
|
-
// Missing key handling
|
|
220
|
-
if (this.config.onMissingKey) {
|
|
221
|
-
this.config.onMissingKey(key, this.locale);
|
|
222
|
-
}
|
|
223
|
-
if (this.config.debug) {
|
|
224
|
-
log(`Missing translation key: ${key} (locale: ${this.locale})`, "warn");
|
|
225
|
-
}
|
|
226
|
-
return key;
|
|
72
|
+
addCustomMessages(locale, messages) {
|
|
73
|
+
this.i18n.addCustomMessages(locale, messages);
|
|
227
74
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
* Replaces {variable} with params.variable
|
|
231
|
-
*/
|
|
232
|
-
interpolate(text, params) {
|
|
233
|
-
if (!params)
|
|
234
|
-
return text;
|
|
235
|
-
return text.replace(/\{(\w+)\}/g, (match, key) => {
|
|
236
|
-
return params[key]?.toString() || match;
|
|
237
|
-
});
|
|
75
|
+
withLocale(locale) {
|
|
76
|
+
return this.i18n.withLocale(locale);
|
|
238
77
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
* Simplified implementation - real implementation would use Intl.PluralRules
|
|
242
|
-
*/
|
|
243
|
-
selectPluralForm(count, locale) {
|
|
244
|
-
// Use Intl.PluralRules if available
|
|
245
|
-
if (typeof Intl !== "undefined" && Intl.PluralRules) {
|
|
246
|
-
try {
|
|
247
|
-
const rules = new Intl.PluralRules(locale);
|
|
248
|
-
const category = rules.select(count);
|
|
249
|
-
return category;
|
|
250
|
-
}
|
|
251
|
-
catch (error) {
|
|
252
|
-
log(`Failed to use Intl.PluralRules for locale: ${locale}`, "warn");
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// Fallback to simple rules
|
|
256
|
-
if (count === 0)
|
|
257
|
-
return "zero";
|
|
258
|
-
if (count === 1)
|
|
259
|
-
return "one";
|
|
260
|
-
return "other";
|
|
78
|
+
formatNumber(value, options) {
|
|
79
|
+
return this.i18n.formatNumber?.(value, options) ?? String(value);
|
|
261
80
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
*/
|
|
265
|
-
detectBrowserLocale() {
|
|
266
|
-
if (typeof navigator === "undefined")
|
|
267
|
-
return "en";
|
|
268
|
-
const browserLang = navigator.language ||
|
|
269
|
-
(navigator.languages && navigator.languages[0]) ||
|
|
270
|
-
"en";
|
|
271
|
-
// Extract language code (e.g., 'en-US' -> 'en')
|
|
272
|
-
return browserLang.split("-")[0];
|
|
81
|
+
formatDate(date, options) {
|
|
82
|
+
return this.i18n.formatDate?.(date, options) ?? date.toISOString();
|
|
273
83
|
}
|
|
274
84
|
}
|
|
@@ -109,6 +109,18 @@ export declare class TTSService {
|
|
|
109
109
|
setHighlightCoordinator(coordinator: HighlightCoordinatorApi): void;
|
|
110
110
|
/**
|
|
111
111
|
* Set a late-bound provider for optional host TTS highlight target remapping.
|
|
112
|
+
*
|
|
113
|
+
* The returned disposer is the only thing that clears the registration: the
|
|
114
|
+
* caller owns the lifetime, and a provider installed once keeps remapping
|
|
115
|
+
* across stops, seek-restarts and later playbacks. Playback termination used to
|
|
116
|
+
* clear it in `stop()` and on both exits of `restartFromSeekIndex`, which left a
|
|
117
|
+
* host that installs before mount — the case a late-bound provider exists for —
|
|
118
|
+
* remapping exactly one playback and then silently falling back to identity.
|
|
119
|
+
*
|
|
120
|
+
* Installing again replaces the previous provider, so a caller that reinstalls
|
|
121
|
+
* per playback needs no disposal between them. A stale provider cannot paint
|
|
122
|
+
* outside its scope in any case: targets are validated by containment in
|
|
123
|
+
* `context.scopeElement` and a failing one falls back to its native range.
|
|
112
124
|
*/
|
|
113
125
|
setHighlightTargetResolverProvider(provider: TTSHighlightTargetResolverProvider | null): () => void;
|
|
114
126
|
/**
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Part of PIE Assessment Toolkit.
|
|
17
17
|
*/
|
|
18
|
-
import { applyMediaFragment } from "./catalog-media.js";
|
|
18
|
+
import { applyMediaFragment, enforceMediaFragment } from "./catalog-media.js";
|
|
19
19
|
import { HighlightColor, HighlightType } from "./HighlightCoordinator.js";
|
|
20
20
|
import { resolveSpokenAudioMedia, } from "./spoken-audio-cards.js";
|
|
21
21
|
import { BrowserTTSProvider } from "./tts/browser-provider.js";
|
|
@@ -282,6 +282,18 @@ export class TTSService {
|
|
|
282
282
|
}
|
|
283
283
|
/**
|
|
284
284
|
* Set a late-bound provider for optional host TTS highlight target remapping.
|
|
285
|
+
*
|
|
286
|
+
* The returned disposer is the only thing that clears the registration: the
|
|
287
|
+
* caller owns the lifetime, and a provider installed once keeps remapping
|
|
288
|
+
* across stops, seek-restarts and later playbacks. Playback termination used to
|
|
289
|
+
* clear it in `stop()` and on both exits of `restartFromSeekIndex`, which left a
|
|
290
|
+
* host that installs before mount — the case a late-bound provider exists for —
|
|
291
|
+
* remapping exactly one playback and then silently falling back to identity.
|
|
292
|
+
*
|
|
293
|
+
* Installing again replaces the previous provider, so a caller that reinstalls
|
|
294
|
+
* per playback needs no disposal between them. A stale provider cannot paint
|
|
295
|
+
* outside its scope in any case: targets are validated by containment in
|
|
296
|
+
* `context.scopeElement` and a failing one falls back to its native range.
|
|
285
297
|
*/
|
|
286
298
|
setHighlightTargetResolverProvider(provider) {
|
|
287
299
|
this.highlightTargetResolverProvider = provider;
|
|
@@ -1731,7 +1743,7 @@ export class TTSService {
|
|
|
1731
1743
|
element.src = applyMediaFragment(source.src, media.fragment);
|
|
1732
1744
|
element.playbackRate = this.normalizePlaybackRate(Number(this.ttsConfig.rate || 1));
|
|
1733
1745
|
await new Promise((resolve, reject) => {
|
|
1734
|
-
let
|
|
1746
|
+
let disposeFragment;
|
|
1735
1747
|
let didStart = false;
|
|
1736
1748
|
let settled = false;
|
|
1737
1749
|
const markStarted = () => {
|
|
@@ -1744,8 +1756,7 @@ export class TTSService {
|
|
|
1744
1756
|
element.removeEventListener("ended", onEnded);
|
|
1745
1757
|
element.removeEventListener("error", onError);
|
|
1746
1758
|
element.removeEventListener("playing", markStarted);
|
|
1747
|
-
|
|
1748
|
-
clearInterval(endGuard);
|
|
1759
|
+
disposeFragment?.();
|
|
1749
1760
|
if (this.activeRecordedAudio?.element === element) {
|
|
1750
1761
|
this.activeRecordedAudio = null;
|
|
1751
1762
|
}
|
|
@@ -1781,16 +1792,9 @@ export class TTSService {
|
|
|
1781
1792
|
element.addEventListener("ended", onEnded);
|
|
1782
1793
|
element.addEventListener("error", onError);
|
|
1783
1794
|
element.addEventListener("playing", markStarted);
|
|
1784
|
-
//
|
|
1785
|
-
//
|
|
1786
|
-
|
|
1787
|
-
const endSeconds = media.fragment?.endSeconds;
|
|
1788
|
-
if (endSeconds !== undefined) {
|
|
1789
|
-
endGuard = setInterval(() => {
|
|
1790
|
-
if (element.currentTime >= endSeconds)
|
|
1791
|
-
onEnded();
|
|
1792
|
-
}, 100);
|
|
1793
|
-
}
|
|
1795
|
+
// Reaching the slice's end is this clip finishing, so the chunk sequence
|
|
1796
|
+
// advances rather than the element merely pausing.
|
|
1797
|
+
disposeFragment = enforceMediaFragment(element, media.fragment, onEnded);
|
|
1794
1798
|
Promise.resolve(element.play()).then(markStarted).catch(onError);
|
|
1795
1799
|
}).finally(() => {
|
|
1796
1800
|
if (this.activeRecordedAudio?.element === element) {
|
|
@@ -2092,7 +2096,6 @@ export class TTSService {
|
|
|
2092
2096
|
return;
|
|
2093
2097
|
this.setState(PlaybackState.IDLE);
|
|
2094
2098
|
this.clearHighlightsAndTracking();
|
|
2095
|
-
this.highlightTargetResolverProvider = null;
|
|
2096
2099
|
}
|
|
2097
2100
|
catch (error) {
|
|
2098
2101
|
if (runId !== this.speakRunId)
|
|
@@ -2100,7 +2103,6 @@ export class TTSService {
|
|
|
2100
2103
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
2101
2104
|
this.setState(PlaybackState.ERROR);
|
|
2102
2105
|
this.clearHighlightsAndTracking();
|
|
2103
|
-
this.highlightTargetResolverProvider = null;
|
|
2104
2106
|
throw error;
|
|
2105
2107
|
}
|
|
2106
2108
|
finally {
|
|
@@ -2205,7 +2207,6 @@ export class TTSService {
|
|
|
2205
2207
|
this.cancelRecordedAudio();
|
|
2206
2208
|
this.provider.onWordBoundary = undefined;
|
|
2207
2209
|
this.provider.stop();
|
|
2208
|
-
this.highlightTargetResolverProvider = null;
|
|
2209
2210
|
this.setState(PlaybackState.IDLE);
|
|
2210
2211
|
this.currentText = null;
|
|
2211
2212
|
// Clear highlights
|