Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<html lang>` 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
`<html lang>` 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`,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2613,8 +2620,9 @@ deliberate additions to it, not drift:
- Content written for an older minor than the one served is upgraded in the frame with the
libraries' own steps. The design serves what a package carries; a bundle that serves newer
minors needs the step the platforms take when they install one.
- The core's own strings follow the element's language, from translations the runtime carries.
The design's frame is English throughout.
- The core's own strings follow the content's language, the one its `h5p.json` names, and the
element's for a package that names none, from translations the runtime carries. The design's
frame is English throughout.
- A package can be played `untrusted`, with library code only from the host's bundle and its
content checked against the libraries' semantics, as the CMS plugins check it. The design runs
whatever a package carries.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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` |
Expand Down
8 changes: 4 additions & 4 deletions apps/demo/demo/two-players.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ <h1>Two on one page</h1>
<p class="lede">
H5P core assumes it owns the page — global <code>H5P</code>, global
<code>H5PIntegration</code>, 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.
</p>

<h5p-player src="/demo/content/how-it-works.h5p"></h5p-player>
<h5p-player src="/demo/content/how-it-works.h5p" lang="de" frame copyright></h5p-player>
<h5p-player src="/demo/content/how-it-works.h5p" frame copyright></h5p-player>

<footer>
<nav class="footer-links" aria-label="More on this site">
Expand Down
2 changes: 1 addition & 1 deletion docs/h5p-standalone.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ loaded it itself; the libraries load in the same order, and the options mean wha
| `customCss`, `customJs` | `custom-css`, `custom-js` | Space separated; their origins are allowed by the frame's CSP |
| `reportingIsEnabled` | `reporting` | |
| `xAPIObjectIRI` | `activity-id` | Defaults to the package URL |
| `translations` | `lang` | H5P's own strings in that language, from the runtime's translations; inherited from the page. Strings of your own cannot be passed |
| `translations` | `lang` | H5P's own strings in the content's language, the one its `h5p.json` names, from the runtime's translations; `lang`, inherited from the page, for a package that names none. Strings of your own cannot be passed |
| `user` | the `user` property | `{ name, mail }`; never stored |
| `metadata`, `title` | — | Read from the package's `h5p.json` |
| `contentUserData`, `saveFreq` | `resume`, `userData`, `save-interval` | |
Expand Down
2 changes: 1 addition & 1 deletion packages/embed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ To update, run the command again with `--force` and redeploy.
| `preload=auto` | Start fetching media at once |
| `activity-id=<IRI>` | The xAPI object id, instead of the package URL |
| `custom-css=<url>` | A stylesheet loaded into the content |
| `lang=<tag>` | The language of H5P's own buttons and dialogs, such as `de` or `pt-BR`. Default: English |
| `lang=<tag>` | The language of H5P's own buttons and dialogs, such as `de` or `pt-BR`, for a package that does not name its own. A package that names its language in `h5p.json` gets them in that language, matching its content. Default: English |
| `xapi=<origin>` | Relay statements to the embedding page |

A custom script and a learner's name cannot be set on the address. Opened unframed, the page plays
Expand Down
3 changes: 2 additions & 1 deletion packages/embed/site/embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,8 @@ export function startEmbed({ packBase = null, pack = null, librariesPack = null,
* that one away, `&refresh-libraries=off` plays a package's own libraries even where the pack
* has newer ones, `&activity-id=` names the statements' object and `&custom-css=` restyles the
* content to the embedding site's taste. `&lang=` is the embedding page's language, which this
* page cannot read across origins: H5P's own buttons and dialogs follow it. Not `custom-js`,
* page cannot read across origins: H5P's own buttons and dialogs follow it for a package that
* does not name its own language, and follow the content's where it does. Not `custom-js`,
* `embed-code` or `user`: a script is a capability on this origin that a link should not hand
* out, the embed is the embed, and a learner's name has no place in a URL.
*/
Expand Down
12 changes: 7 additions & 5 deletions packages/embed/tests/site.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,19 +249,21 @@ describe('a written site, framed from another site', () => {
await page.close()
})

it('shows H5P\'s own strings in the language the address names', async () => {
it('hands the element the language the address names, and shows H5P\'s own strings in the content\'s', async () => {
const page = await browser.newPage()
await page.goto(`${embedding.origin}/open-de.html`)
const frame = await playerFrame(page, open.origin)
await frame.waitForFunction(() => (document.querySelector('h5p-player') as unknown as { state: string } | null)?.state === 'ready', null, { timeout: 60_000 })
// The runtime's frame, inside the page's own: the same origin, so its window can be read.
const seen = await frame.evaluate(() => {
const inner = document.querySelector('h5p-player')!.shadowRoot!.querySelector('iframe')!
const player = document.querySelector('h5p-player')!
const inner = player.shadowRoot!.querySelector('iframe')!
const core = (inner.contentWindow as unknown as { H5P: { t(key: string): string } }).H5P
return { lang: inner.contentDocument!.documentElement.lang, fullscreen: core.t('fullscreen') }
return { element: player.getAttribute('lang'), lang: inner.contentDocument!.documentElement.lang, fullscreen: core.t('fullscreen') }
})
// The quiz is in English, which stays the document's language for a screen reader; the buttons are German.
expect(seen).toEqual({ lang: 'en', fullscreen: 'Vollbild' })
// The quiz says it is English, so its buttons are too, on a German page: the address's
// language is for a package that names none (#55).
expect(seen).toEqual({ element: 'de', lang: 'en', fullscreen: 'Fullscreen' })
await page.close()
})

Expand Down
17 changes: 17 additions & 0 deletions packages/player/scripts/build-fixtures.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,23 @@ built.push([
})
])

// 1c. The ordinary case saying which language it is in, as `h5p.json`'s `defaultLanguage` does:
// H5P's own strings follow it, whatever the page's language (#55). `basic.h5p` names none,
// so it plays in the element's.
for (const [name, language] of [
['german.h5p', 'de'],
['english.h5p', 'en']
]) {
built.push([
name,
await writeArchive(name, async (writer) => {
const manifest = JSON.parse(await readFile(resolve(sourceDir, 'h5p.json'), 'utf8'))
await writer.add('h5p.json', new TextReader(JSON.stringify({ ...manifest, language, defaultLanguage: language })))
await addBaseFiles(writer, { skip: ['h5p.json'] })
})
])
}

// 2. A large deflated entry: the Jobs worker inflates it into chunks and the virtual server
// serves the prefix that exists as a shorter 206.
built.push([
Expand Down
2 changes: 2 additions & 0 deletions packages/player/scripts/lib/fixture-names.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const LARGE_ENTRY_SIZE = 20 * 1024 * 1024

export const GENERATED_FIXTURES = [
'basic.h5p',
'german.h5p',
'english.h5p',
'finished.h5p',
'large-deflated.h5p',
'large-stored.h5p',
Expand Down
7 changes: 4 additions & 3 deletions packages/player/src/h5p-offline-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1294,9 +1294,10 @@ export class H5PPlayerElement extends HTMLElement {
}

/**
* The language the frame is in, read by the load like the attributes the record keeps: the
* element's `lang`, or the one it inherits from the page. The worker writes it as the frame
* document's language, and the runtime's boot script picks its translation of the core's
* The element's language, read by the load like the attributes the record keeps: its `lang`,
* or the one it inherits from the page. The frame is in the content's own language, the one
* `h5p.json` names, and H5P's own strings with it; this one is the frame's only for a package
* that names none, and the runtime's boot script then picks its translation of the core's
* strings for it, or leaves them English.
*/
private frameLanguage(): string | undefined {
Expand Down
Loading
Loading