diff --git a/README.md b/README.md index 2318e4b..93d0fcd 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,10 @@ decks-as-code/ │ ├── build-manifest.mjs regenerates decks.json │ ├── build-site.mjs builds dist/: offline site plus single-file decks │ ├── import-pptx.mjs converts a .pptx into a Markdown deck folder -│ └── pptx-to-html.mjs converts a .pptx into an HTML deck folder (pptx-html.mjs) +│ ├── pptx-to-html.mjs converts a .pptx into an HTML deck folder (pptx-html.mjs) +│ ├── translate.mjs translates a deck's strings.json with an LLM +│ ├── translate-core.mjs hashing, validation, batching, retry (used by translate) +│ └── llm-provider.mjs provider interface: copilot, openai-compatible, mock └── decks/ └── / ├── slides.md the deck. The only file you edit day to day. @@ -166,7 +169,8 @@ Open `convert.html` on the served site (or `node tools/pptx-to-html.mjs talk.ppt The folder holds `slides.html`, `strings.json` and `images/`. `strings.json` lists every paragraph, speaker note and alt text under a stable ID, in the format described in [`docs/string-table.md`](docs/string-table.md); translations are `strings..json` -files in the same folder, and `?lang=` on the deck or standalone file shows one. +files in the same folder. A language picker appears in the corner of the deck (and the +single-file version) when translations exist; `?lang=` opens one directly. Longer translated text shrinks to fit its box (down to 60%); add `&review` to the address to outline boxes that still overflow. @@ -185,6 +189,47 @@ table, so nothing translates them. --- +## Translate a deck + +`tools/translate.mjs` translates a deck's string table (`decks//strings.json`, +see [`docs/string-table.md`](docs/string-table.md)) into `strings..json` files. +Node 18+, no dependencies. + +```powershell +node tools\translate.mjs my-talk --to de,pt-BR,ar # translate +node tools\translate.mjs my-talk --to de --dry-run # show what would change +node tools\translate.mjs my-talk --to de --force # retranslate everything +node tools\translate.mjs my-talk --to de --provider mock # offline fake, no LLM +node --test # run the tests (no network) +``` + +- **Incremental.** Only strings that are missing or whose source text changed (the + `src` hash no longer matches) are sent to the model. `--force` redoes all of them. +- **Validated.** Every translation must keep the same inline tags (in matching pairs), + keep `href` values, and be non-empty. A failing batch is retried once with the error + fed back; strings that still fail are skipped and listed, the file stays valid, and + the viewer falls back to the source text for them. Exit code `2` means "done, but + some strings were skipped"; re-run to retry them. +- **Provider.** `--provider copilot` (default) runs the GitHub Copilot CLI in + non-interactive mode, so it uses the models in your Copilot subscription: no API key, + nothing to configure beyond signing in once by running `copilot`. Tools, MCP servers and + custom instructions are switched off, so the model can only answer. Optional: + `--model ` (otherwise the CLI's default), `LLM_REASONING_EFFORT` (default `low`), + `COPILOT_BIN` if `copilot` is not on your PATH. The five-slide sample takes under a + minute per language; a 50-slide deck takes a few minutes. + `--provider openai` talks to any OpenAI-compatible + `/chat/completions` endpoint. Configure it with environment variables (the key is + never a flag, never logged, never written): `LLM_API_KEY` (or `OPENAI_API_KEY`), + `LLM_MODEL` (or `--model`), and optionally `LLM_BASE_URL` (or `--base-url`; default + `https://api.openai.com/v1`). For Azure OpenAI / Foundry use the + `https://.openai.azure.com/openai/v1` base URL, the deployment name as the + model, and `LLM_AUTH_HEADER=api-key`. Set `LLM_JSON_MODE=off` if a host rejects + `response_format`. 429s honour `retry-after`. Providers live in + `tools/llm-provider.mjs`; adding one means returning `{ name, complete({messages}) }`. + GitHub Models was retired on 2026-07-30, so it is not supported. + +--- + ## Writing slides `slides.md` is ordinary Markdown with three separator conventions: diff --git a/assets/deck-i18n.js b/assets/deck-i18n.js index 21ea18b..574ae88 100644 --- a/assets/deck-i18n.js +++ b/assets/deck-i18n.js @@ -114,7 +114,62 @@ if (reveal) fitSlide(reveal.getCurrentSlide()); } + function languageName(code) { + try { + var label = new Intl.DisplayNames([code], { type: 'language' }).of(code); + return label ? label.charAt(0).toUpperCase() + label.slice(1) : code; + } catch (e) { + return code; + } + } + + /** + * A language