Skip to content

feat(i18n): YAML-driven translations with EN/ES + language switcher#397

Merged
tonyalaribe merged 1 commit into
monoscope-tech:masterfrom
victalejo:i18n-yaml
May 25, 2026
Merged

feat(i18n): YAML-driven translations with EN/ES + language switcher#397
tonyalaribe merged 1 commit into
monoscope-tech:masterfrom
victalejo:i18n-yaml

Conversation

@victalejo

Copy link
Copy Markdown
Contributor

Re-scope of #388 — auth + admin + Telegram/webhook channels are pulled out into separate follow-up PRs. This one focuses only on i18n, with the per-language YAML layout suggested in #388.

Why

Spanish (my native language). I'm not proposing a full i18n overhaul — just the groundwork so other languages can be added later via issues/community requests, rather than hardcoding everything in English. — context from the closed PR

This PR adds that groundwork. EN ships with all existing strings translated (drop-in: same text as today) and ES ships as the first non-English language. Other languages can be added by a contributor who doesn't know Haskell: drop a new YAML file, wire one constructor.

How

  • Translations as data. Per-language strings live as flat-key YAML in static/i18n/<code>.yaml:

    auth.login.title:  "Sign in"
    nav.dashboards:    "Dashboards"

    YAML is embedded into the binary at compile time via file-embed, so the runtime stays pure and a malformed file fails the build (not a request). Missing keys fall through to the raw key — typos surface as visible nav.dashboards strings in the UI rather than blanks.

  • New Web.I18n module with Language(En, Es), t :: Language -> Text -> Text, and cookie helpers.

  • Session gains a lang :: Language field, read from the lang cookie at session-build time next to theme / isSidebarClosed. Templates render with t sess.lang \"key\".

  • GET /set_language/<code>?redirect_to=… persists the cookie (Max-Age = 1 year) and bounces back. The dropdown switcher rewrites redirect_to to the current path on click so users don't get bounced to /.

  • Adding a new language is data-only after Haskell support:

    1. static/i18n/fr.yaml
    2. Add Fr constructor to Language, one case in parseLanguage + languageCode
    3. Rebuild

What's NOT in this PR (deliberate)

  • Local password auth / register / login page → separate PR
  • Admin instance settings (SMTP, Telegram bot token) → separate PR
  • Telegram / Webhook notification channels → separate PR
  • Onboarding restructure → separate PR
  • Translation of every deep page (LogExplorer, Monitors, etc.) → I focused on high-traffic surfaces (sidebar nav, user menu, the bits visible everywhere). Deeper translation can be added incrementally by translators just by editing the YAMLs.

Tested locally

  • gh auth status / git diff against monoscope-tech/master — only the 9 files listed below.
  • Self-hosted build (matching CI Dockerfile path) succeeded; sidebar/user menu render in both languages; switcher persists across page loads via cookie.

Files

File Lines
monoscope.cabal +2
src/Models/Projects/Projects.hs +5
src/Pages/BodyWrapper.hs +35 / -6
src/Pkg/TestUtils.hs +3 / -2
src/Web/Auth.hs +21 / -3
src/Web/I18n.hs +91 (new)
src/Web/Routes.hs +2
static/i18n/en.yaml +179 (new)
static/i18n/es.yaml +179 (new)

Happy to iterate — let me know if the YAML format should be nested (auth: login: title:) instead of flat (auth.login.title:), or if there's a preferred location other than static/i18n/.

Per-language strings live in static/i18n/<code>.yaml. Two languages ship
today (en, es). Adding a third only needs a new YAML file, a constructor
in 'Language', and the matching cases in 'parseLanguage'/'languageCode'.

- New 'Web.I18n' module: 'Language' (En, Es), 't lang key' lookup,
  cookie helpers ('languageFromCookies', 'languageSetCookieBS').
  YAML is embedded into the binary at compile time via 'file-embed' so
  the runtime stays pure and decode errors fail the build.
- 'Projects.Session' gains a 'lang :: Language' field; the value is read
  from the 'lang' cookie at session-build time alongside theme/isSidebarClosed.
- 'GET /set_language/<code>?redirect_to=…' persists the cookie (Max-Age 1yr)
  and bounces the user back.
- Sidebar nav items and the user-menu dropdown render via 't sess.lang';
  the dropdown also shows the EN/ES switcher (rewrites href to current path
  on click so users don't get bounced to /).
- Tests: 'testSessionHeader' / 'refreshSession' pass a default language
  through the new 'sessionByID' parameter.

Missing keys fall through to the raw key, so typos surface as visible
'nav.dashboards' strings in the UI rather than blanks.
@tonyalaribe

Copy link
Copy Markdown
Contributor

Thanks for the pr @victalejo . Merging it

@tonyalaribe
tonyalaribe merged commit 24d4d15 into monoscope-tech:master May 25, 2026
2 of 5 checks passed
tonyalaribe added a commit that referenced this pull request May 26, 2026
… patterns

- PR #397 added file-embed dep to monoscope.cabal by hand but not package.yaml; next hpack run would wipe it. Mirror the dep in package.yaml so they stay in sync.
- LogPatterns.UpsertPattern gained an isError field; mkPattern/mkPatternWithSample test helpers were missing it, causing -Werror=missing-fields failures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants