From e8a58f0ff2d945525e3aa352656562ad26a7405b Mon Sep 17 00:00:00 2001 From: Aleksandr Shevchuk Date: Tue, 20 Oct 2026 14:34:55 +0300 Subject: [PATCH 1/3] feat: H5P's own strings follow the content's language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H5P's buttons and dialogs followed the element's `lang`, usually the page's, while the content's own text — its questions, its Check and Retry — is in the language its author wrote it in. A page whose language differed showed both at once: "Vollbild" around an English quiz. The worker now writes the frame in the content's language, `h5p.json`'s `defaultLanguage`, which it already used for the frame's ``, and H5P's strings take the same one. `lang` decides only for a package that names no language, or `und`; English stays the default and the fallback. The embed's `&lang=` is the same fallback, and its test now expects the English quiz in English on a German page. The two-players example drops `lang="de"`, which no longer changed anything for its English package. Closes #55. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 27 ++++++----- README.md | 6 +-- apps/demo/demo/two-players.html | 8 ++-- docs/h5p-standalone.md | 2 +- packages/embed/README.md | 2 +- packages/embed/site/embed.js | 3 +- packages/embed/tests/site.test.ts | 12 ++--- packages/player/scripts/build-fixtures.mjs | 17 +++++++ packages/player/src/h5p-offline-player.ts | 7 +-- packages/player/src/sw/frame-document.ts | 33 +++++++------- packages/player/tests/browser/l10n.test.ts | 31 ++++++++++--- packages/player/tests/browser/utils.ts | 3 ++ .../player/tests/unit/frame-document.test.ts | 45 +++++++++++++------ skills/h5p-player-setup/SKILL.md | 5 ++- 14 files changed, 134 insertions(+), 67 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2cb13ce..bbc9c8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -785,16 +785,21 @@ element, and the element acts. That relay is why `frame-document.ts` has a `mess and upstream's checks against the parent's semantics. `packages/runtime/tests/` pins the rules; `tests/browser/upgrades.test.ts` plays exports written for 1.0, 1.1 and 1.2, sub-content included, against a bundle at 1.2, and one whose step throws. -- **The core's strings follow the element's language.** Built on 2026-10-12, to match the CMS +- **The core's strings follow the content's language.** Built on 2026-10-12, to match the CMS plugins, which show H5P's own strings — the fullscreen button, "Rights of use" and the licence names in it, the "this content has changed" dialog — in the site's language; h5p-standalone's are English. The element reads its `lang`, its own or inherited across a shadow root to its host - (`declaredLanguage`), and the record keeps the tag; unless it is English, the worker names the - runtime's `l10n.js` before the boot script and puts the tag in the boot configuration. The - frame's `` is the content's, not that tag: `h5p.json`'s `defaultLanguage`, which H5P - writes into the iframe it makes, so a screen reader reads an English quiz on a German page in - English with German buttons around it — the first cut wrote the element's tag there, and review - caught it the same day — and the element's tag only for a package that names none. That one + (`declaredLanguage`), and the record keeps the tag. The frame's language is the content's: + `h5p.json`'s `defaultLanguage`, which H5P writes into the iframe it makes, and the element's tag + only for a package that names none (`frameLanguage` in `frame-document.ts`); unless it is + English, the worker names the runtime's `l10n.js` before the boot script and puts the tag in the + boot configuration. Until 2026-10-20 (#55) the strings followed the element's tag and only the + `` the content's, so an English quiz on a German page had "Vollbild" and + "Nutzungsrechte" around its own Check and Retry, which its author wrote into the package. The + owner's rule: the interface and the content in one language. A select for the language, on the + player page and in the app, was built the same day and closed unmerged (#53, #54) for the same + reason: it could only make the two differ. `lang` is the fallback, and so is the embed page's + `&lang=`. `german.h5p` and `english.h5p`, `basic.h5p` naming its language, are the fixtures. That one file has every language and the aliases; the boot script picks one by RFC 4647 lookup through the aliases (`src/l10n.ts` in the runtime — `de-AT` gets `de`, `nb` Norwegian) and lays it over the core's own English, `getLocalization()` at the vendored commit (`vendor/h5p-core/strings.json`, @@ -831,7 +836,8 @@ element, and the element acts. That relay is why `frame-document.ts` has a `mess Moodle's English added, "Attribution (CC BY)". `tests/l10n.test.ts` in the runtime holds every file and alias to those rules and pins the lookup; `tests/browser/l10n.test.ts` plays German, Brazilian Portuguese, Austrian German, Bokmål, Greek one string short, English and Thai, and a - `runtime` with and without `l10nJs`; the embed's `site.test.ts` an English quiz on a German page. + `runtime` with and without `l10nJs`, and a German and an English package each in their own + language whatever the page's; the embed's `site.test.ts` an English quiz on a German page, in English. - **An unreachable library source falls back to a bundle downloaded from it before.** When the probe of `libraries`' URL fails — offline, the hub down, a 5xx — the element asks the worker (`find-downloaded-libraries`) for the newest `role: 'libraries'` row with the same URL whose @@ -2412,8 +2418,9 @@ SPA-fallback trap described above cannot happen there. like the site failing rather than a demonstration. Building it found `build-demo-content.mjs` failing for every package since the hub helpers moved: its loop ran as the module loaded, before the helpers below it were declared. The two-players example plays - `how-it-works.h5p` twice since the same day, the second `lang="de" frame copyright`, since - one package under two settings is what the per-load fix is for. The outputs + `how-it-works.h5p` twice since the same day, the second `frame copyright`, since one package + under two settings is what the per-load fix is for; it was `lang="de"` too until #55, when an + English package's strings became English whatever the element's language. The outputs are committed, 5.5 MB for the four, because a deploy should need neither the hub nor the video host; `pnpm demo:content` regenerates them. Provenance: the libraries are MIT, the text is ours under CC0, and the clip is Big Buck Bunny, © Blender Foundation, CC BY 3.0, credited in the diff --git a/README.md b/README.md index afbfb81..4083d66 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ learners and your content. [build it came from](https://github.com/missing-elements/h5p-offline-player/blob/main/docs/revision.md), `finished` is the content's own result, [cmi5](https://github.com/missing-elements/h5p-offline-player/blob/main/docs/cmi5.md) launches it from an LMS, and [resume](https://github.com/missing-elements/h5p-offline-player/blob/main/docs/resume.md) keeps the learner's place. -- **Works offline, in the learner's language.** The installable app plays with no network, on iOS - too, and H5P's own buttons follow `lang` in more than fifty languages. Results stay on your page +- **Works offline, in the content's language.** The installable app plays with no network, on iOS + too, and H5P's own buttons match the language the content is written in, any of more than fifty. Results stay on your page unless your code sends them; [privacy](https://github.com/missing-elements/h5p-offline-player/blob/main/docs/privacy.md) lists what the player fetches, and what it keeps on the device. - **Comes with tools for pipelines.** [`h5p-verify`](https://github.com/missing-elements/h5p-offline-player/blob/main/docs/verify.md) plays a package headless @@ -193,7 +193,7 @@ allows one Service Worker, or must work offline, mounts the player in its own wo | `custom-css`, `custom-js` | attribute | Stylesheets and scripts loaded in the frame after the runtime's own | | `reporting` | attribute | Enables the submit button in content types that have one | | `activity-id` | attribute | The statements' object id. Default: the package URL | -| `lang` | attribute | The language of H5P's own buttons and dialogs, such as "Fullscreen" and "Rights of use", in more than fifty languages. Inherited from the page like any element's language, and read at each load. Default: English | +| `lang` | attribute | The language of H5P's own buttons and dialogs, such as "Fullscreen" and "Rights of use", for a package that does not name its own. A package that names its language in `h5p.json` (`defaultLanguage`), as H5P's exports do, gets them in that language, so they match the content's own text whatever the page's language. More than fifty languages. Inherited from the page like any element's language, and read at each load. Default: English | | `user` | property | `{ name, mail }`, the actor of every statement. Set before `src` | | `auto-resize` | attribute | `off` to size the element yourself. Default: follows the content's height | | `preload` | attribute, property | `auto` pulls large deflated media before it is asked for. Default: `none` | diff --git a/apps/demo/demo/two-players.html b/apps/demo/demo/two-players.html index 8d07892..9c92678 100644 --- a/apps/demo/demo/two-players.html +++ b/apps/demo/demo/two-players.html @@ -33,13 +33,13 @@

Two on one page

H5P core assumes it owns the page — global H5P, global H5PIntegration, global CSS. Each player gets its own frame document, so two of - them on one page do not collide. Below, the same package twice: the second in German, with - the action bar, which the first does not have. The archive is read once and cached for - both; what each player asked for stays its own. + them on one page do not collide. Below, the same package twice: the second with the action + bar, which the first does not have. The archive is read once and cached for both; what each + player asked for stays its own.

- +