From 538b28e5d27574acbf1dea159545b95a26320745 Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 21 Jul 2026 18:24:09 +0200 Subject: [PATCH] Lang menu and autodetect --- site/assets/css/main.css | 52 ++++++++++++++++++++++++-- site/i18n/en.json | 3 ++ site/i18n/es.json | 3 ++ site/i18n/pt.json | 3 ++ site/layouts/partials/lang-switch.html | 48 ++++++++++++++++-------- site/nginx.conf | 37 ++++++++++++++++++ 6 files changed, 128 insertions(+), 18 deletions(-) diff --git a/site/assets/css/main.css b/site/assets/css/main.css index 68effd2..9cb1bc9 100644 --- a/site/assets/css/main.css +++ b/site/assets/css/main.css @@ -71,9 +71,55 @@ a { color: var(--green); } } .site-nav a { color: #fff; text-decoration: none; opacity: .95; white-space: nowrap; } .site-nav a:hover { text-decoration: underline; } -.lang-switch { display: inline-flex; gap: .6rem; font-size: .92rem; white-space: nowrap; } -.lang-switch .current { opacity: .7; } -.lang-switch a.lang { color: #fff; } +/* A typical globe+code language menu (
/, no JS) instead of + spelling out every language name in the header — scales past 3 languages + without crowding the About/Legal links. */ +.lang-switch { + position: relative; + font-size: .9rem; +} +.lang-switch summary { + display: inline-flex; + align-items: center; + gap: .35rem; + cursor: pointer; + list-style: none; + padding: .3rem .6rem; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, .4); + color: #fff; +} +.lang-switch summary::-webkit-details-marker { display: none; } +.lang-switch summary::after { content: "▾"; font-size: .7em; opacity: .85; } +.lang-switch[open] summary::after { content: "▴"; } +.lang-switch summary:hover { border-color: #fff; } +.lang-code { font-weight: 600; letter-spacing: .02em; } +.lang-menu { + position: absolute; + inset-inline-end: 0; + top: calc(100% + .4rem); + margin: 0; + padding: .35rem; + list-style: none; + min-width: 9rem; + background: #fff; + color: var(--title); + border-radius: 10px; + box-shadow: 0 8px 24px rgba(0, 0, 0, .22); + z-index: 10; +} +.lang-menu li + li { margin-top: .1rem; } +.lang-menu a.lang, +.lang-menu span.current { + display: block; + padding: .45rem .6rem; + border-radius: 6px; + text-decoration: none; + white-space: nowrap; +} +.lang-menu a.lang { color: var(--title); } +.lang-menu a.lang:hover { background: var(--container); } +.lang-menu span.current { color: var(--muted); font-weight: 600; } /* Narrow phones: brand on its own row, nav wraps beneath it, left-aligned. */ @media (max-width: 560px) { diff --git a/site/i18n/en.json b/site/i18n/en.json index b6d4d3f..bca3d1c 100644 --- a/site/i18n/en.json +++ b/site/i18n/en.json @@ -11,6 +11,9 @@ "navHome": { "other": "Home" }, + "navLanguage": { + "other": "Language" + }, "screenshotsTitle": { "other": "A look inside" }, diff --git a/site/i18n/es.json b/site/i18n/es.json index 7c6ec53..4a3ebab 100644 --- a/site/i18n/es.json +++ b/site/i18n/es.json @@ -11,6 +11,9 @@ "navHome": { "other": "Inicio" }, + "navLanguage": { + "other": "Idioma" + }, "screenshotsTitle": { "other": "Un vistazo por dentro" }, diff --git a/site/i18n/pt.json b/site/i18n/pt.json index e65af48..67373e9 100644 --- a/site/i18n/pt.json +++ b/site/i18n/pt.json @@ -11,6 +11,9 @@ "navHome": { "other": "Início" }, + "navLanguage": { + "other": "Idioma" + }, "screenshotsTitle": { "other": "Um vislumbre por dentro" }, diff --git a/site/layouts/partials/lang-switch.html b/site/layouts/partials/lang-switch.html index 356d294..5f61839 100644 --- a/site/layouts/partials/lang-switch.html +++ b/site/layouts/partials/lang-switch.html @@ -1,16 +1,34 @@ -{{- /* Links to this page in the other language; falls back to that language's home. */ -}} - - {{- range .Site.Languages -}} - {{- $lang := . -}} - {{- if eq $lang.Lang $.Site.Language.Lang -}} - {{ $lang.LanguageName }} - {{- else -}} - {{- $target := "" -}} - {{- range $.AllTranslations -}} - {{- if eq .Language.Lang $lang.Lang -}}{{- $target = .RelPermalink -}}{{- end -}} - {{- end -}} - {{- if not $target -}}{{- $target = ($lang.Lang | printf "/%s/" | relURL) -}}{{- end -}} - {{ $lang.LanguageName }} +{{- /* + A typical "globe + code" language menu, done with
/ so it + needs no JS (site stays static per config.toml). Links to this page in the + other language; falls back to that language's home. +*/ -}} +
+ + {{- /* Material Icons "translate" glyph (文A) — the same icon the app uses + for its language picker (Icons.translate in settings_screen.dart). + Inline so it needs no icon font or external request (self-contained + per CSP). */ -}} + + {{ .Site.Language.LanguageCode | upper }} + +
    + {{- range .Site.Languages -}} + {{- $lang := . -}} +
  • + {{- if eq $lang.Lang $.Site.Language.Lang -}} + {{ $lang.LanguageName }} + {{- else -}} + {{- $target := "" -}} + {{- range $.AllTranslations -}} + {{- if eq .Language.Lang $lang.Lang -}}{{- $target = .RelPermalink -}}{{- end -}} + {{- end -}} + {{- if not $target -}}{{- $target = ($lang.Lang | printf "/%s/" | relURL) -}}{{- end -}} + {{ $lang.LanguageName }} + {{- end -}} +
  • {{- end -}} - {{- end -}} - +
+
diff --git a/site/nginx.conf b/site/nginx.conf index 9dce0bb..0c87910 100644 --- a/site/nginx.conf +++ b/site/nginx.conf @@ -1,9 +1,46 @@ +# Auto-detect language from the browser's Accept-Language header, no JS +# involved (site stays static/no-JS per config.toml). Only the exact "/" is +# affected — deep links (e.g. someone shares /es/legal/rules/) are untouched. +# Fires once per visitor: a "tane_visited" cookie (set on every response) is +# checked first, so a later visit to "/" — including clicking "English" from +# the switcher on a non-English browser — is never redirected again. +map $http_accept_language $tane_lang { + default en; + ~*^es es; + ~*^pt pt; +} + +map $http_cookie $tane_seen { + default 0; + "~*tane_visited=1" 1; +} + server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; + # Non-sensitive, no PII: only remembers "this browser has been here + # before" so the auto-redirect below doesn't repeat. No Secure flag since + # the value carries no sensitive data and this must also work over plain + # http in local/dev previews; TLS is terminated upstream in production. + add_header Set-Cookie "tane_visited=1; Path=/; Max-Age=31536000; SameSite=Lax" always; + + location = / { + set $tane_redirect ""; + if ($tane_seen = 0) { + set $tane_redirect $tane_lang; + } + if ($tane_redirect = en) { + set $tane_redirect ""; + } + if ($tane_redirect != "") { + return 302 /$tane_redirect/; + } + try_files /index.html =404; + } + # Security headers. The page is fully static and self-contained: inline CSS # (needs style-src 'unsafe-inline'), same-origin images (+ data: URIs), no # third-party scripts. HSTS is host-scoped (no includeSubDomains) so it can't