Skip to content

MIP-0054 tasks

Ordered delivery of MIP-0054 as stacked PRs (.claude/skills/mip-tasks/SKILL.md, scripts/stack.sh). Branch mip-0054/<k>-<slug>, each based on the previous; merge bottom-up, scripts/stack.sh restack after each squash-merge.

This round is the site's half and the codes that close it, re-planned on 2026-09-30 for §5.8 (#412): per-locale JSON catalogs, the gates, and the toggle next to the GitHub link. Tasks 1 and 2 are the page, and task 3 is the Scala change that lets the page translate the scoring notes. Nothing waits on another MIP any more: the toggle no longer lives in MIP-0044's menu, and MIP-0046 task 1 rebases over t() rather than landing first (decision 6).

# slug delivers tests (must exist before the PR) depends on
1 i18n-chrome §5.8 and §5.5's language half. site/i18n/pt-BR.json, site/i18n/en.json (ICU subset: {arg}, plural, select) and site/i18n/context.json (one note per key) with every static string of index.html, about.html and chat.js: nav, controls, hour label, legend, day labels, status, footer blurb, chat, noscript. scripts/i18n_bundle.py writes site/static/i18n.js and generates the x-pseudo locale; --check runs gates 1-3 and fails on a stale bundle; --self-test; both wired into just quality-other and CI. site/static/ui.js: the pure resolveLang({param, stored, languages, supported}), t(key, args) with the ICU subset over Intl.PluralRules/Intl.NumberFormat (falling back to pt-BR, then the key), applyLang() over data-i18n, data-i18n-title, data-i18n-aria-label and data-i18n-placeholder, plus document.title and <html lang>, and setParam, which moves from app.js. #lang: two <button aria-pressed> (pt, en, each with its own lang and an endonym aria-label) in .sitenav, immediately left of a.gh, on both pages, writing localStorage['marola.lang'] (try/catch) and ?lang=. Both HTML files' source text becomes pt-BR with <html lang="pt-BR">; about.html's article carries both languages as article[lang] blocks shown by :lang(). site.yml's publish allowlist gains ui.js and i18n.js scripts/i18n_bundle.py --self-test: a missing key, an empty value, an argument missing in one locale, an unparsable message, a key with no context note and a stale bundle each fail --check. scripts/site_check.js: <html lang="pt-BR"> in both files; resolveLang returns pt-BR for no input, en for ?lang=en, pt-BR for a stored en plus ?lang=pt-BR, en for languages ['en-US'], pt-BR for ['fr-FR'], and x-pseudo only from the param; a localStorage that throws still resolves; #lang is two <button>s with aria-pressed, placed before a.gh inside .sitenav on both pages, and a click flips them and writes the store and the URL; every data-i18n* key exists; t pluralises {n, plural, one {# praia} other {# praias}} for 1 and 2 and formats 1.3 as 1,3 in pt-BR; values are lowercase except an allowlist (°C, UV, km/h, PRÓPRIA, IMPRÓPRIA, GitHub, Open-Meteo, IMA/SC, OpenStreetMap). Screenshots at 390 px and 1280 px in both languages, nav included –
2 i18n-app Every string app.js and chat.js build goes through t(): tooltip and card cells, the list, the hour label, status and fail(), the footer, the near-me notices, the chat panel. renderDays is split out of selectArea, so a language flip relabels the days without a fetch, and an onLang hook re-renders in place. wind_level, jellyfish and whales values render through lvl.* and wind.* keys, with unknown values passed through. Numbers and distances go through Intl.NumberFormat. noteText() renders t('note.' + code, args) when the board carries note_codes and the English notes string otherwise, with the note.* keys task 3 emits, in both languages. Gate 4: the x-pseudo run scripts/site_check.js, per language: the default run shows hour-label "melhor hora por praia", a tooltip with "água-viva", and 1/1 PRÓPRIA (25 Aug) untouched (the board's own month, until messages-report); a ?lang=en run shows today's English needles; a synthetic board with note_codes renders a note.* key in both languages; the real fixture, without codes, shows notes verbatim; an x-pseudo run of both pages finds no visible text outside t() beyond an allowlist of data values (beach names, provider names, verdict tokens, numbers); the marker-path and band-class assertions are unchanged. Screenshots of the tooltip and the card in both languages 1
3 note-codes §5.1's board half, in Scala. Swimability returns Note(code: NoteCode, args) instead of String; Board writes hours[].note_codes and best.note_codes next to notes, which stay; board.schema.json's schema enum becomes [1, 2]; Report's English wording is unchanged; site/fixtures/board.json gains codes, and every emitted code has a note.* key in both catalogs SwimabilitySpec and WaterVerdictSpec assert on NoteCodes, not English strings. BoardSpec: note_codes present, schema 2, and a schema-1 fixture still validates. scripts/site_check.js: the fixture validates against the schema, and task 2's hook renders its real codes in both languages. sbt test, scalafmtCheckAll and scalafixAll --check green 2

Later (not filed)

  • ja-locale: site/i18n/ja.json, the :lang(ja) CSS of §5.8 (no case transform, word-break: auto-phrase with fallback, local Japanese font names), the verdict gloss keys, and the third toggle button, added only after a native-speaker review (§11). Depends on 2.
  • locale-paths: /pt-br/, /en/, /ja/ trees with hreflang alternates, and / redirecting by the same resolveLang (§5.8 portability). Depends on 2.
  • beach-names-i18n: the board carries OSM's name:en/name:ja when present, and the page picks by locale. A board-contract change after 3.
  • messages-report: marola.i18n.Messages, Report in pt-BR, --lang and MAROLA_LANG, and water.summary's month per language (§5.1, §3's CLI block). Decides which catalog is canonical for the note.* overlap (§11). Depends on 3.
  • prompts-pt: the pt-BR DSPy recompile of both prompts (§5.2). Depends on messages-report.
  • corpus-pt: knowledge/pt-BR/, sourced line by line (§5.3).
  • language-guard: LanguageGuard and the benchmark's pt-BR arm (§5.4, §7). Depends on messages-report and corpus-pt.
  • area-default: §5.5's location half on the site: a stored area, the nearest area on "near me", the timezone hint, and §3's outside-coverage line. Client-only, and the natural next task to file.

Decisions

  1. (Superseded by decision 6.) The site's catalog is a JS object in ui.js, not data/i18n/*.json fetched at load. No request before the first render, no flash of the wrong language, works from file://, and no Scala in the site's half. Recorded in §5.1.
  2. pt-BR is the last fallback, as §5.5 says. In practice navigator.languages decides for almost every visitor, and the constant only matters to a browser that exposes no language.
  3. Mixed language between tasks 1 and 3 is accepted. The chrome turns Portuguese while the notes, the water summary's month and the lore stay English (§8). Task 3 closes the notes.
  4. The release blocker is unchanged. Release 0 still waits for the whole MIP, later tasks included; this round does not make the release shippable on its own.
  5. A native-speaker pass gates the release, not task 1's merge (§11), since the page is English only today.
  6. Re-planned 2026-09-30 (#412). The catalog moved from a JS object in ui.js to per-locale JSON with a generated i18n.js (§5.8), the gates joined tasks 1 and 2, and the toggle moved next to the GitHub link by the maintainer's decision. That drops 0044-T1 and 0046-T1 as dependencies. MIP-0046 task 1 now rebases over the t() calls instead of landing first, which reverses MIP-0046.tasks.md's decision 1 for this pair.