@intellectif/lk-react 7.0.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +8 -3
  3. package/dist/{WrittenResponse-zWT-Go1g.d.cts → WrittenResponse-BJr9gBpY.d.cts} +3 -1
  4. package/dist/{WrittenResponse-BJL4dGsR.d.ts → WrittenResponse-Du5CZnwn.d.ts} +3 -1
  5. package/dist/{chunk-UFWF3BKA.cjs → chunk-2VLKSNYJ.cjs} +13 -24
  6. package/dist/chunk-2VLKSNYJ.cjs.map +1 -0
  7. package/dist/{chunk-PPJ47THY.cjs → chunk-4AHZBTBF.cjs} +27 -29
  8. package/dist/chunk-4AHZBTBF.cjs.map +1 -0
  9. package/dist/{chunk-PGEG7UZJ.cjs → chunk-4XPKKNJQ.cjs} +5 -5
  10. package/dist/chunk-4XPKKNJQ.cjs.map +1 -0
  11. package/dist/{chunk-52A5UNCH.js → chunk-7OHPBLPH.js} +13 -24
  12. package/dist/chunk-7OHPBLPH.js.map +1 -0
  13. package/dist/{chunk-ABHJW4LC.cjs → chunk-7VIXWVFN.cjs} +11 -14
  14. package/dist/chunk-7VIXWVFN.cjs.map +1 -0
  15. package/dist/chunk-AHEV52U7.cjs +122 -0
  16. package/dist/chunk-AHEV52U7.cjs.map +1 -0
  17. package/dist/{chunk-7YLY3G3E.js → chunk-BLQBFI7Z.js} +23 -25
  18. package/dist/chunk-BLQBFI7Z.js.map +1 -0
  19. package/dist/{chunk-VHULVDPG.js → chunk-EQ5BZTLV.js} +29 -14
  20. package/dist/chunk-EQ5BZTLV.js.map +1 -0
  21. package/dist/{chunk-6F4X2NKL.js → chunk-ESWHIC4Z.js} +5 -5
  22. package/dist/chunk-ESWHIC4Z.js.map +1 -0
  23. package/dist/chunk-G3QPOYDR.cjs +198 -0
  24. package/dist/chunk-G3QPOYDR.cjs.map +1 -0
  25. package/dist/{chunk-XS2SZINH.js → chunk-HEJPEJW7.js} +10 -13
  26. package/dist/chunk-HEJPEJW7.js.map +1 -0
  27. package/dist/{chunk-C3HR55JD.js → chunk-MMFHKCWF.js} +24 -13
  28. package/dist/chunk-MMFHKCWF.js.map +1 -0
  29. package/dist/{chunk-3VQV7ICQ.js → chunk-OI4IKH2Z.js} +18 -18
  30. package/dist/chunk-OI4IKH2Z.js.map +1 -0
  31. package/dist/{chunk-XGQKID2P.cjs → chunk-SGIUPGDA.cjs} +29 -18
  32. package/dist/chunk-SGIUPGDA.cjs.map +1 -0
  33. package/dist/chunk-T4CKODCJ.js +115 -0
  34. package/dist/chunk-T4CKODCJ.js.map +1 -0
  35. package/dist/{chunk-6DSA75TA.cjs → chunk-VVZ7SAE7.cjs} +35 -20
  36. package/dist/chunk-VVZ7SAE7.cjs.map +1 -0
  37. package/dist/chunk-WJTCIW5R.js +196 -0
  38. package/dist/chunk-WJTCIW5R.js.map +1 -0
  39. package/dist/{chunk-FB6XKQFV.cjs → chunk-ZDB3SLYR.cjs} +23 -23
  40. package/dist/chunk-ZDB3SLYR.cjs.map +1 -0
  41. package/dist/components/ActivityPreview.cjs +20 -0
  42. package/dist/components/ActivityPreview.cjs.map +1 -0
  43. package/dist/components/ActivityPreview.d.cts +70 -0
  44. package/dist/components/ActivityPreview.d.ts +70 -0
  45. package/dist/components/ActivityPreview.js +11 -0
  46. package/dist/components/ActivityPreview.js.map +1 -0
  47. package/dist/components/ActivitySequence.cjs +9 -8
  48. package/dist/components/ActivitySequence.d.cts +19 -6
  49. package/dist/components/ActivitySequence.d.ts +19 -6
  50. package/dist/components/ActivitySequence.js +8 -7
  51. package/dist/components/FillInTheBlanks.cjs +5 -4
  52. package/dist/components/FillInTheBlanks.d.cts +1 -1
  53. package/dist/components/FillInTheBlanks.d.ts +1 -1
  54. package/dist/components/FillInTheBlanks.js +4 -3
  55. package/dist/components/MultipleChoice.cjs +5 -4
  56. package/dist/components/MultipleChoice.d.cts +1 -1
  57. package/dist/components/MultipleChoice.d.ts +1 -1
  58. package/dist/components/MultipleChoice.js +4 -3
  59. package/dist/components/StimulusPanel.cjs +4 -3
  60. package/dist/components/StimulusPanel.d.cts +4 -2
  61. package/dist/components/StimulusPanel.d.ts +4 -2
  62. package/dist/components/StimulusPanel.js +3 -2
  63. package/dist/components/WrittenResponse.cjs +5 -4
  64. package/dist/components/WrittenResponse.d.cts +3 -3
  65. package/dist/components/WrittenResponse.d.ts +3 -3
  66. package/dist/components/WrittenResponse.js +4 -3
  67. package/dist/i18n/LkIntlProvider.cjs +33 -0
  68. package/dist/i18n/LkIntlProvider.cjs.map +1 -0
  69. package/dist/i18n/LkIntlProvider.d.cts +71 -0
  70. package/dist/i18n/LkIntlProvider.d.ts +71 -0
  71. package/dist/i18n/LkIntlProvider.js +4 -0
  72. package/dist/i18n/LkIntlProvider.js.map +1 -0
  73. package/dist/index.cjs +47 -17
  74. package/dist/index.cjs.map +1 -1
  75. package/dist/index.d.cts +5 -3
  76. package/dist/index.d.ts +5 -3
  77. package/dist/index.js +10 -8
  78. package/dist/index.js.map +1 -1
  79. package/dist/{types-eXX4hzSj.d.ts → strings-vQDqESTS.d.cts} +175 -11
  80. package/dist/{types-eXX4hzSj.d.cts → strings-vQDqESTS.d.ts} +175 -11
  81. package/dist/theme/skin.css +4 -4
  82. package/package.json +23 -3
  83. package/dist/chunk-3VQV7ICQ.js.map +0 -1
  84. package/dist/chunk-52A5UNCH.js.map +0 -1
  85. package/dist/chunk-6DSA75TA.cjs.map +0 -1
  86. package/dist/chunk-6F4X2NKL.js.map +0 -1
  87. package/dist/chunk-7YLY3G3E.js.map +0 -1
  88. package/dist/chunk-ABHJW4LC.cjs.map +0 -1
  89. package/dist/chunk-C3HR55JD.js.map +0 -1
  90. package/dist/chunk-FB6XKQFV.cjs.map +0 -1
  91. package/dist/chunk-PGEG7UZJ.cjs.map +0 -1
  92. package/dist/chunk-PPJ47THY.cjs.map +0 -1
  93. package/dist/chunk-UFWF3BKA.cjs.map +0 -1
  94. package/dist/chunk-VHULVDPG.js.map +0 -1
  95. package/dist/chunk-XGQKID2P.cjs.map +0 -1
  96. package/dist/chunk-XS2SZINH.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { ActivityData, ActivityResult, LearnerResponse, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry } from '@intellectif/lk-core';
1
+ import { ActivityData, ActivityResult, LearnerResponse, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry, Stimulus } from '@intellectif/lk-core';
2
2
 
3
3
  /**
4
4
  * How an activity is being presented. This is the single switch that decides
@@ -116,9 +116,26 @@ interface ActivityProps<TData extends ActivityData = ActivityData> {
116
116
  mediaBudget?: MediaBudgetBinding;
117
117
  /** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */
118
118
  mediaStrings?: Partial<MediaTransportStrings>;
119
+ /**
120
+ * Overrides the SDK's chrome text for this activity, layered on whatever
121
+ * `LkIntlProvider` supplies. `mediaStrings` still works and is merged after
122
+ * this, so an existing 0.8.x call site keeps behaving as it did.
123
+ */
124
+ strings?: LkStringsOverride;
119
125
  onInteraction?: (event: InteractionEvent) => void;
120
126
  /** Per-instance token overrides, applied as inline CSS vars on the root. */
121
127
  theme?: Partial<ThemeTokens>;
128
+ /**
129
+ * BCP 47 tag stamped as `lang` on this component's root. This is the
130
+ * INTERFACE language — the SDK's own chrome renders inside that element — so
131
+ * it should carry the same value you give `<LkIntlProvider locale>`. Passing
132
+ * a different one re-declares the language of every SDK string in this
133
+ * subtree without changing the words.
134
+ *
135
+ * It is NOT `data.locale`, which labels xAPI statements only. Authored
136
+ * content in another language belongs on `stimulus.locale`, which
137
+ * `<StimulusPanel>` puts on the passage alone.
138
+ */
122
139
  locale?: string;
123
140
  disabled?: boolean;
124
141
  }
@@ -171,14 +188,11 @@ declare function asRenderable<TData extends ActivityData>(redacted: RedactedActi
171
188
  * />
172
189
  * ```
173
190
  *
174
- * Pass `renderMode="exam"` (or `"review"`). Redacted data has no answer key,
175
- * and the default `practice` mode grades locally — so `<MultipleChoice>` and
176
- * `<FillInTheBlanks>` throw at render rather than fail at submit time.
177
- *
178
- * `<WrittenResponse>` is the exception: it never grades on the client and has
179
- * no such guard, so a redacted essay renders and stays answerable in
180
- * `practice`. A mis-wired essay item is therefore SILENT — set `renderMode`
181
- * explicitly rather than relying on the throw.
191
+ * Pass `renderMode="exam"` (or `"review"`). Redacted data has no answer key, and
192
+ * all three built-in activities throw at render in the default `practice` mode
193
+ * rather than fail later: `<MultipleChoice>` and `<FillInTheBlanks>` because
194
+ * they grade locally, and `<WrittenResponse>` because `practice` still runs its
195
+ * local submit path and emits a practice-mode xAPI statement.
182
196
  */
183
197
  declare function asRenderableSequence(entries: readonly (RedactedActivityData | RedactedItemGroupData)[]): readonly SequenceEntry<RenderableActivity>[];
184
198
  /** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */
@@ -186,7 +200,12 @@ type RedactedItemGroupData = ItemGroup<RedactedActivityData> & {
186
200
  redacted: true;
187
201
  };
188
202
  /**
189
- * Every English string the SDK's audio transport renders.
203
+ * Every word the SDK's audio transport renders.
204
+ *
205
+ * Words, not characters: the `m:ss / m:ss` clock is digits and punctuation and
206
+ * is formatted by the component, because it reads identically in every locale
207
+ * this SDK targets. The scrubber's SPOKEN value does have a word in it and does
208
+ * have a key ({@link MediaTransportStrings.timeValue}).
190
209
  *
191
210
  * Supply them to translate it. These are the highest-stakes strings on a
192
211
  * listening paper — "No plays remaining" decides whether a learner believes
@@ -204,6 +223,12 @@ interface MediaTransportStrings {
204
223
  volume: string;
205
224
  speed: string;
206
225
  seek: string;
226
+ /**
227
+ * Spoken value of the scrubber, e.g. `('1:05', '4:30') => '1:05 of 4:30'`.
228
+ * Takes ALREADY-FORMATTED `m:ss` strings: a translation should not have to
229
+ * reimplement the clock to change the word between them.
230
+ */
231
+ timeValue: (elapsed: string, duration: string) => string;
207
232
  /** e.g. `(1, 2) => '1 of 2 plays remaining'`. */
208
233
  playsRemaining: (remaining: number, max: number) => string;
209
234
  noPlaysRemaining: string;
@@ -304,4 +329,143 @@ interface SequenceMediaBudget {
304
329
  strings?: Partial<MediaTransportStrings>;
305
330
  }
306
331
 
307
- export { type ActivityProps as A, type HtmlSanitizer as H, type MediaBudgetBinding as M, type RenderMode as R, type SequenceMediaBudget as S, type MediaTransportStrings as a, type Renderable as b, type RenderableActivity as c, asRenderable as d, asRenderableSequence as e };
332
+ /**
333
+ * Every string the SDK's own chrome renders.
334
+ *
335
+ * Until 0.8.1 these were literals, and `locale` set only the `lang` attribute —
336
+ * so a learner sitting a Spanish paper read a Spanish passage inside an English
337
+ * scaffold, and "No plays remaining" on a listening exam was untranslatable.
338
+ * This is the whole surface, in one place, so a consumer can replace it.
339
+ *
340
+ * **Interpolation and plurals are functions, not format strings.** A message
341
+ * like `"Question {n} of {total}"` needs a parser, and a parser needs plural
342
+ * rules for every locale the SDK does not know. A function hands both problems
343
+ * to the consumer, who has `Intl.PluralRules` and knows their own language:
344
+ *
345
+ * ```ts
346
+ * questionProgress: (index, total) => `Pregunta ${index} de ${total}`,
347
+ * wordCount: (n) => `${n} ${n === 1 ? 'palabra' : 'palabras'}`,
348
+ * ```
349
+ *
350
+ * It also lets a translation reorder its arguments, which a positional token
351
+ * cannot, and TypeScript checks the arity. No parser, no message catalogue
352
+ * format, no runtime dependency.
353
+ *
354
+ * **What is deliberately NOT here: thrown errors.** Every `throw` in this
355
+ * package is addressed to the developer who wired the component up, not to the
356
+ * learner. Translating them would make them unsearchable and would put the one
357
+ * audience who can act on them behind a translation. They stay English.
358
+ */
359
+ interface LkStrings {
360
+ /** Multiple Choice submit button, and Written Response. */
361
+ submit: string;
362
+ /** Fill-in-the-Blanks in `practice`: the learner is checking their own work. */
363
+ checkAnswers: string;
364
+ /** Fill-in-the-Blanks in `exam`: nothing is checked on the client. */
365
+ submitAnswers: string;
366
+ /**
367
+ * The scored result, read by a screen reader on submit.
368
+ *
369
+ * `percent` is already rounded to a whole number. The authored overall
370
+ * feedback is appended SEPARATELY by the component, so a translation of this
371
+ * sentence never has to carry the author's words — the two were previously
372
+ * concatenated into one literal, which made the score sentence untranslatable
373
+ * without also touching authored content.
374
+ */
375
+ scoreAnnouncement: (percent: number, passed: boolean) => string;
376
+ /** Prefix announced on submit, before the score sentence. */
377
+ answerSubmitted: string;
378
+ /** An answer submitted for grading that has none yet. Never rendered as 0%. */
379
+ notGradedYet: string;
380
+ /** An outcome that carries no grade and never will. */
381
+ noGradeAvailable: string;
382
+ showFeedback: string;
383
+ hideFeedback: string;
384
+ /** Accessible name of a blank's input. `ordinal` is 1-based. */
385
+ blankLabel: (ordinal: number) => string;
386
+ showHint: string;
387
+ hideHint: string;
388
+ /**
389
+ * Announced when an essay is handed in. Distinct from {@link LkStrings.awaitingGrade},
390
+ * which is the `review`-mode state of a submission made earlier: this one
391
+ * confirms the act, and is the only thing a learner hears at the moment they
392
+ * commit work that nothing on the client will grade.
393
+ */
394
+ responseSubmitted: string;
395
+ /** Live word counter. Needs the count for languages whose plural rules differ. */
396
+ wordCount: (count: number) => string;
397
+ /** The authored bounds, e.g. "50–200 words". `min` is 0 when unbounded below. */
398
+ wordBounds: (min: number, max: number) => string;
399
+ /** A rubric criterion the grader marked not applicable. */
400
+ notApplicable: string;
401
+ /** A returned grade flagged for a human to look at — learner-visible by design. */
402
+ awaitingHumanReview: string;
403
+ /** Submitted, no grade back yet. */
404
+ awaitingGrade: string;
405
+ /** The grader could not produce a grade. */
406
+ couldNotBeGraded: string;
407
+ previous: string;
408
+ next: string;
409
+ /** Progress label, also the question region's accessible name. `index` is 1-based. */
410
+ questionProgress: (index: number, total: number) => string;
411
+ /** Shown when no renderer is registered for an activity type. */
412
+ unsupportedActivity: string;
413
+ /**
414
+ * `<ActivityPreview>`, in place of a draft that still has something missing.
415
+ * Author-facing, not learner-facing — but an editor is translated too.
416
+ */
417
+ previewIncomplete: string;
418
+ /** `<ActivityPreview>`, in place of a draft with something wrong in it. */
419
+ previewInvalid: string;
420
+ /**
421
+ * Accessible name of a provider iframe whose author supplied no `alt`.
422
+ * `MediaSchema` requires `alt` on an embed, so validated content never
423
+ * reaches this — but `validateActivity` is opt-in, and a screen-reader
424
+ * learner meeting an unvalidated one should not meet it in English.
425
+ */
426
+ embeddedMedia: string;
427
+ /**
428
+ * Accessible name of the stimulus region when the author gave it no title.
429
+ * Keyed by `Stimulus['kind']`, because "Recording" and "Passage" do not share
430
+ * a gender or an article in every language.
431
+ */
432
+ stimulusKind: Record<Stimulus['kind'], string>;
433
+ /**
434
+ * How far the material carries, e.g. "Questions 3–8". Called with equal
435
+ * `first` and `last` for a single question, so a translation can choose its
436
+ * own singular rather than receive a pre-pluralised English one.
437
+ */
438
+ stimulusRange: (first: number, last: number) => string;
439
+ /** Production fallback when an activity throws during render. */
440
+ activityFailed: string;
441
+ /** The same, when the activity's title is known. */
442
+ activityFailedNamed: (title: string) => string;
443
+ /** Production fallback when the activity has no title to name. */
444
+ activityFailedUnnamed: string;
445
+ /**
446
+ * The listening-paper transport. Shipped in 0.8.0 as its own `mediaStrings`
447
+ * prop, which still works — this is the same shape, reachable from the one
448
+ * provider so a consumer does not wire two mechanisms.
449
+ */
450
+ media: MediaTransportStrings;
451
+ }
452
+ /** A partial override, nested one level for {@link LkStrings.media}. */
453
+ type LkStringsOverride = Partial<Omit<LkStrings, 'media' | 'stimulusKind'>> & {
454
+ media?: Partial<MediaTransportStrings>;
455
+ stimulusKind?: Partial<Record<Stimulus['kind'], string>>;
456
+ };
457
+ /**
458
+ * English defaults, byte-identical to what every component rendered before the
459
+ * strings surface existed. Supplying no override changes nothing on screen.
460
+ */
461
+ declare const DEFAULT_STRINGS: LkStrings;
462
+ /**
463
+ * Layers an override onto a base dictionary.
464
+ *
465
+ * Two levels deep and no further, because the shape is two levels deep. A
466
+ * generic deep merge would silently accept a nested object where a function
467
+ * belongs and fail at render instead of at the call site.
468
+ */
469
+ declare function mergeStrings(base: LkStrings, override?: LkStringsOverride): LkStrings;
470
+
471
+ export { type ActivityProps as A, DEFAULT_STRINGS as D, type HtmlSanitizer as H, type LkStrings as L, type MediaBudgetBinding as M, type RenderMode as R, type SequenceMediaBudget as S, type LkStringsOverride as a, type MediaTransportStrings as b, type Renderable as c, type RenderableActivity as d, asRenderable as e, asRenderableSequence as f, mergeStrings as m };
@@ -1,4 +1,4 @@
1
- import { ActivityData, ActivityResult, LearnerResponse, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry } from '@intellectif/lk-core';
1
+ import { ActivityData, ActivityResult, LearnerResponse, ItemOutcome, MediaPlayLedgerEntry, MediaPlayClaim, MediaPlayGrant, InteractionEvent, ThemeTokens, RedactedActivityData, ItemGroup, SequenceEntry, Stimulus } from '@intellectif/lk-core';
2
2
 
3
3
  /**
4
4
  * How an activity is being presented. This is the single switch that decides
@@ -116,9 +116,26 @@ interface ActivityProps<TData extends ActivityData = ActivityData> {
116
116
  mediaBudget?: MediaBudgetBinding;
117
117
  /** Translations for the audio transport chrome. See {@link MediaTransportStrings}. */
118
118
  mediaStrings?: Partial<MediaTransportStrings>;
119
+ /**
120
+ * Overrides the SDK's chrome text for this activity, layered on whatever
121
+ * `LkIntlProvider` supplies. `mediaStrings` still works and is merged after
122
+ * this, so an existing 0.8.x call site keeps behaving as it did.
123
+ */
124
+ strings?: LkStringsOverride;
119
125
  onInteraction?: (event: InteractionEvent) => void;
120
126
  /** Per-instance token overrides, applied as inline CSS vars on the root. */
121
127
  theme?: Partial<ThemeTokens>;
128
+ /**
129
+ * BCP 47 tag stamped as `lang` on this component's root. This is the
130
+ * INTERFACE language — the SDK's own chrome renders inside that element — so
131
+ * it should carry the same value you give `<LkIntlProvider locale>`. Passing
132
+ * a different one re-declares the language of every SDK string in this
133
+ * subtree without changing the words.
134
+ *
135
+ * It is NOT `data.locale`, which labels xAPI statements only. Authored
136
+ * content in another language belongs on `stimulus.locale`, which
137
+ * `<StimulusPanel>` puts on the passage alone.
138
+ */
122
139
  locale?: string;
123
140
  disabled?: boolean;
124
141
  }
@@ -171,14 +188,11 @@ declare function asRenderable<TData extends ActivityData>(redacted: RedactedActi
171
188
  * />
172
189
  * ```
173
190
  *
174
- * Pass `renderMode="exam"` (or `"review"`). Redacted data has no answer key,
175
- * and the default `practice` mode grades locally — so `<MultipleChoice>` and
176
- * `<FillInTheBlanks>` throw at render rather than fail at submit time.
177
- *
178
- * `<WrittenResponse>` is the exception: it never grades on the client and has
179
- * no such guard, so a redacted essay renders and stays answerable in
180
- * `practice`. A mis-wired essay item is therefore SILENT — set `renderMode`
181
- * explicitly rather than relying on the throw.
191
+ * Pass `renderMode="exam"` (or `"review"`). Redacted data has no answer key, and
192
+ * all three built-in activities throw at render in the default `practice` mode
193
+ * rather than fail later: `<MultipleChoice>` and `<FillInTheBlanks>` because
194
+ * they grade locally, and `<WrittenResponse>` because `practice` still runs its
195
+ * local submit path and emits a practice-mode xAPI statement.
182
196
  */
183
197
  declare function asRenderableSequence(entries: readonly (RedactedActivityData | RedactedItemGroupData)[]): readonly SequenceEntry<RenderableActivity>[];
184
198
  /** Structural shape of a `redactItemGroup()` projection, as it arrives from a server. */
@@ -186,7 +200,12 @@ type RedactedItemGroupData = ItemGroup<RedactedActivityData> & {
186
200
  redacted: true;
187
201
  };
188
202
  /**
189
- * Every English string the SDK's audio transport renders.
203
+ * Every word the SDK's audio transport renders.
204
+ *
205
+ * Words, not characters: the `m:ss / m:ss` clock is digits and punctuation and
206
+ * is formatted by the component, because it reads identically in every locale
207
+ * this SDK targets. The scrubber's SPOKEN value does have a word in it and does
208
+ * have a key ({@link MediaTransportStrings.timeValue}).
190
209
  *
191
210
  * Supply them to translate it. These are the highest-stakes strings on a
192
211
  * listening paper — "No plays remaining" decides whether a learner believes
@@ -204,6 +223,12 @@ interface MediaTransportStrings {
204
223
  volume: string;
205
224
  speed: string;
206
225
  seek: string;
226
+ /**
227
+ * Spoken value of the scrubber, e.g. `('1:05', '4:30') => '1:05 of 4:30'`.
228
+ * Takes ALREADY-FORMATTED `m:ss` strings: a translation should not have to
229
+ * reimplement the clock to change the word between them.
230
+ */
231
+ timeValue: (elapsed: string, duration: string) => string;
207
232
  /** e.g. `(1, 2) => '1 of 2 plays remaining'`. */
208
233
  playsRemaining: (remaining: number, max: number) => string;
209
234
  noPlaysRemaining: string;
@@ -304,4 +329,143 @@ interface SequenceMediaBudget {
304
329
  strings?: Partial<MediaTransportStrings>;
305
330
  }
306
331
 
307
- export { type ActivityProps as A, type HtmlSanitizer as H, type MediaBudgetBinding as M, type RenderMode as R, type SequenceMediaBudget as S, type MediaTransportStrings as a, type Renderable as b, type RenderableActivity as c, asRenderable as d, asRenderableSequence as e };
332
+ /**
333
+ * Every string the SDK's own chrome renders.
334
+ *
335
+ * Until 0.8.1 these were literals, and `locale` set only the `lang` attribute —
336
+ * so a learner sitting a Spanish paper read a Spanish passage inside an English
337
+ * scaffold, and "No plays remaining" on a listening exam was untranslatable.
338
+ * This is the whole surface, in one place, so a consumer can replace it.
339
+ *
340
+ * **Interpolation and plurals are functions, not format strings.** A message
341
+ * like `"Question {n} of {total}"` needs a parser, and a parser needs plural
342
+ * rules for every locale the SDK does not know. A function hands both problems
343
+ * to the consumer, who has `Intl.PluralRules` and knows their own language:
344
+ *
345
+ * ```ts
346
+ * questionProgress: (index, total) => `Pregunta ${index} de ${total}`,
347
+ * wordCount: (n) => `${n} ${n === 1 ? 'palabra' : 'palabras'}`,
348
+ * ```
349
+ *
350
+ * It also lets a translation reorder its arguments, which a positional token
351
+ * cannot, and TypeScript checks the arity. No parser, no message catalogue
352
+ * format, no runtime dependency.
353
+ *
354
+ * **What is deliberately NOT here: thrown errors.** Every `throw` in this
355
+ * package is addressed to the developer who wired the component up, not to the
356
+ * learner. Translating them would make them unsearchable and would put the one
357
+ * audience who can act on them behind a translation. They stay English.
358
+ */
359
+ interface LkStrings {
360
+ /** Multiple Choice submit button, and Written Response. */
361
+ submit: string;
362
+ /** Fill-in-the-Blanks in `practice`: the learner is checking their own work. */
363
+ checkAnswers: string;
364
+ /** Fill-in-the-Blanks in `exam`: nothing is checked on the client. */
365
+ submitAnswers: string;
366
+ /**
367
+ * The scored result, read by a screen reader on submit.
368
+ *
369
+ * `percent` is already rounded to a whole number. The authored overall
370
+ * feedback is appended SEPARATELY by the component, so a translation of this
371
+ * sentence never has to carry the author's words — the two were previously
372
+ * concatenated into one literal, which made the score sentence untranslatable
373
+ * without also touching authored content.
374
+ */
375
+ scoreAnnouncement: (percent: number, passed: boolean) => string;
376
+ /** Prefix announced on submit, before the score sentence. */
377
+ answerSubmitted: string;
378
+ /** An answer submitted for grading that has none yet. Never rendered as 0%. */
379
+ notGradedYet: string;
380
+ /** An outcome that carries no grade and never will. */
381
+ noGradeAvailable: string;
382
+ showFeedback: string;
383
+ hideFeedback: string;
384
+ /** Accessible name of a blank's input. `ordinal` is 1-based. */
385
+ blankLabel: (ordinal: number) => string;
386
+ showHint: string;
387
+ hideHint: string;
388
+ /**
389
+ * Announced when an essay is handed in. Distinct from {@link LkStrings.awaitingGrade},
390
+ * which is the `review`-mode state of a submission made earlier: this one
391
+ * confirms the act, and is the only thing a learner hears at the moment they
392
+ * commit work that nothing on the client will grade.
393
+ */
394
+ responseSubmitted: string;
395
+ /** Live word counter. Needs the count for languages whose plural rules differ. */
396
+ wordCount: (count: number) => string;
397
+ /** The authored bounds, e.g. "50–200 words". `min` is 0 when unbounded below. */
398
+ wordBounds: (min: number, max: number) => string;
399
+ /** A rubric criterion the grader marked not applicable. */
400
+ notApplicable: string;
401
+ /** A returned grade flagged for a human to look at — learner-visible by design. */
402
+ awaitingHumanReview: string;
403
+ /** Submitted, no grade back yet. */
404
+ awaitingGrade: string;
405
+ /** The grader could not produce a grade. */
406
+ couldNotBeGraded: string;
407
+ previous: string;
408
+ next: string;
409
+ /** Progress label, also the question region's accessible name. `index` is 1-based. */
410
+ questionProgress: (index: number, total: number) => string;
411
+ /** Shown when no renderer is registered for an activity type. */
412
+ unsupportedActivity: string;
413
+ /**
414
+ * `<ActivityPreview>`, in place of a draft that still has something missing.
415
+ * Author-facing, not learner-facing — but an editor is translated too.
416
+ */
417
+ previewIncomplete: string;
418
+ /** `<ActivityPreview>`, in place of a draft with something wrong in it. */
419
+ previewInvalid: string;
420
+ /**
421
+ * Accessible name of a provider iframe whose author supplied no `alt`.
422
+ * `MediaSchema` requires `alt` on an embed, so validated content never
423
+ * reaches this — but `validateActivity` is opt-in, and a screen-reader
424
+ * learner meeting an unvalidated one should not meet it in English.
425
+ */
426
+ embeddedMedia: string;
427
+ /**
428
+ * Accessible name of the stimulus region when the author gave it no title.
429
+ * Keyed by `Stimulus['kind']`, because "Recording" and "Passage" do not share
430
+ * a gender or an article in every language.
431
+ */
432
+ stimulusKind: Record<Stimulus['kind'], string>;
433
+ /**
434
+ * How far the material carries, e.g. "Questions 3–8". Called with equal
435
+ * `first` and `last` for a single question, so a translation can choose its
436
+ * own singular rather than receive a pre-pluralised English one.
437
+ */
438
+ stimulusRange: (first: number, last: number) => string;
439
+ /** Production fallback when an activity throws during render. */
440
+ activityFailed: string;
441
+ /** The same, when the activity's title is known. */
442
+ activityFailedNamed: (title: string) => string;
443
+ /** Production fallback when the activity has no title to name. */
444
+ activityFailedUnnamed: string;
445
+ /**
446
+ * The listening-paper transport. Shipped in 0.8.0 as its own `mediaStrings`
447
+ * prop, which still works — this is the same shape, reachable from the one
448
+ * provider so a consumer does not wire two mechanisms.
449
+ */
450
+ media: MediaTransportStrings;
451
+ }
452
+ /** A partial override, nested one level for {@link LkStrings.media}. */
453
+ type LkStringsOverride = Partial<Omit<LkStrings, 'media' | 'stimulusKind'>> & {
454
+ media?: Partial<MediaTransportStrings>;
455
+ stimulusKind?: Partial<Record<Stimulus['kind'], string>>;
456
+ };
457
+ /**
458
+ * English defaults, byte-identical to what every component rendered before the
459
+ * strings surface existed. Supplying no override changes nothing on screen.
460
+ */
461
+ declare const DEFAULT_STRINGS: LkStrings;
462
+ /**
463
+ * Layers an override onto a base dictionary.
464
+ *
465
+ * Two levels deep and no further, because the shape is two levels deep. A
466
+ * generic deep merge would silently accept a nested object where a function
467
+ * belongs and fail at render instead of at the call site.
468
+ */
469
+ declare function mergeStrings(base: LkStrings, override?: LkStringsOverride): LkStrings;
470
+
471
+ export { type ActivityProps as A, DEFAULT_STRINGS as D, type HtmlSanitizer as H, type LkStrings as L, type MediaBudgetBinding as M, type RenderMode as R, type SequenceMediaBudget as S, type LkStringsOverride as a, type MediaTransportStrings as b, type Renderable as c, type RenderableActivity as d, asRenderable as e, asRenderableSequence as f, mergeStrings as m };
@@ -260,7 +260,7 @@
260
260
  }
261
261
 
262
262
  .lk-mc-option-feedback {
263
- margin-left: auto;
263
+ margin-inline-start: auto;
264
264
  font-size: var(--lk-font-size-sm);
265
265
  color: var(--lk-color-text-muted);
266
266
  }
@@ -338,7 +338,7 @@
338
338
 
339
339
  /* Inline per-blank feedback shown after submission. */
340
340
  .lk-fib-blank-feedback {
341
- margin-left: var(--lk-spacing-xs);
341
+ margin-inline-start: var(--lk-spacing-xs);
342
342
  font-size: var(--lk-font-size-sm);
343
343
  color: var(--lk-color-text-muted);
344
344
  }
@@ -376,7 +376,7 @@
376
376
  .lk-fib-blank [aria-live]:not(:empty) {
377
377
  position: absolute;
378
378
  bottom: calc(100% + 6px);
379
- left: 0;
379
+ inset-inline-start: 0;
380
380
  z-index: 1;
381
381
  max-width: 16rem;
382
382
  padding: var(--lk-spacing-xs) var(--lk-spacing-sm);
@@ -393,7 +393,7 @@
393
393
  content: "";
394
394
  position: absolute;
395
395
  top: 100%;
396
- left: var(--lk-spacing-sm);
396
+ inset-inline-start: var(--lk-spacing-sm);
397
397
  border: 5px solid transparent;
398
398
  border-top-color: var(--lk-color-text);
399
399
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectif/lk-react",
3
- "version": "7.0.1",
3
+ "version": "8.0.0",
4
4
  "description": "Accessible React 19 assessment components — multiple choice, fill-in-the-blanks, written response — with a resumable exam/review pager, theming, and xAPI delivery.",
5
5
  "keywords": [
6
6
  "react",
@@ -43,6 +43,16 @@
43
43
  "default": "./dist/index.cjs"
44
44
  }
45
45
  },
46
+ "./components/ActivityPreview": {
47
+ "import": {
48
+ "types": "./dist/components/ActivityPreview.d.ts",
49
+ "default": "./dist/components/ActivityPreview.js"
50
+ },
51
+ "require": {
52
+ "types": "./dist/components/ActivityPreview.d.cts",
53
+ "default": "./dist/components/ActivityPreview.cjs"
54
+ }
55
+ },
46
56
  "./components/ActivitySequence": {
47
57
  "import": {
48
58
  "types": "./dist/components/ActivitySequence.d.ts",
@@ -113,6 +123,16 @@
113
123
  "default": "./dist/hooks/useXAPI.cjs"
114
124
  }
115
125
  },
126
+ "./i18n/LkIntlProvider": {
127
+ "import": {
128
+ "types": "./dist/i18n/LkIntlProvider.d.ts",
129
+ "default": "./dist/i18n/LkIntlProvider.js"
130
+ },
131
+ "require": {
132
+ "types": "./dist/i18n/LkIntlProvider.d.cts",
133
+ "default": "./dist/i18n/LkIntlProvider.cjs"
134
+ }
135
+ },
116
136
  "./theme/ThemeProvider": {
117
137
  "import": {
118
138
  "types": "./dist/theme/ThemeProvider.d.ts",
@@ -137,7 +157,7 @@
137
157
  "peerDependencies": {
138
158
  "react": "^19",
139
159
  "react-dom": "^19",
140
- "@intellectif/lk-core": "^0.8.1"
160
+ "@intellectif/lk-core": "^0.9.0"
141
161
  },
142
162
  "devDependencies": {
143
163
  "@axe-core/react": "^4.10.0",
@@ -156,7 +176,7 @@
156
176
  "tsup": "^8.3.0",
157
177
  "vitest": "^4.1.6",
158
178
  "vitest-axe": "^0.1.0",
159
- "@intellectif/lk-core": "0.8.1"
179
+ "@intellectif/lk-core": "0.9.0"
160
180
  },
161
181
  "license": "MIT",
162
182
  "engines": {