Lang menu and autodetect

This commit is contained in:
vjrj 2026-07-21 18:24:09 +02:00
parent 1b439e9499
commit 93574abc58
6 changed files with 128 additions and 18 deletions

View file

@ -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 (<details>/<summary>, 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) {

View file

@ -11,6 +11,9 @@
"navHome": {
"other": "Home"
},
"navLanguage": {
"other": "Language"
},
"screenshotsTitle": {
"other": "A look inside"
},

View file

@ -11,6 +11,9 @@
"navHome": {
"other": "Inicio"
},
"navLanguage": {
"other": "Idioma"
},
"screenshotsTitle": {
"other": "Un vistazo por dentro"
},

View file

@ -11,6 +11,9 @@
"navHome": {
"other": "Início"
},
"navLanguage": {
"other": "Idioma"
},
"screenshotsTitle": {
"other": "Um vislumbre por dentro"
},

View file

@ -1,16 +1,34 @@
{{- /* Links to this page in the other language; falls back to that language's home. */ -}}
<span class="lang-switch">
{{- range .Site.Languages -}}
{{- $lang := . -}}
{{- if eq $lang.Lang $.Site.Language.Lang -}}
<span class="lang current" aria-current="true">{{ $lang.LanguageName }}</span>
{{- else -}}
{{- $target := "" -}}
{{- range $.AllTranslations -}}
{{- if eq .Language.Lang $lang.Lang -}}{{- $target = .RelPermalink -}}{{- end -}}
{{- end -}}
{{- if not $target -}}{{- $target = ($lang.Lang | printf "/%s/" | relURL) -}}{{- end -}}
<a class="lang" href="{{ $target }}" lang="{{ $lang.LanguageCode }}">{{ $lang.LanguageName }}</a>
{{- /*
A typical "globe + code" language menu, done with <details>/<summary> 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.
*/ -}}
<details class="lang-switch">
<summary aria-label="{{ T "navLanguage" }}">
{{- /* 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). */ -}}
<svg class="lang-globe" aria-hidden="true" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/>
</svg>
<span class="lang-code">{{ .Site.Language.LanguageCode | upper }}</span>
</summary>
<ul class="lang-menu">
{{- range .Site.Languages -}}
{{- $lang := . -}}
<li>
{{- if eq $lang.Lang $.Site.Language.Lang -}}
<span class="lang current" aria-current="true">{{ $lang.LanguageName }}</span>
{{- else -}}
{{- $target := "" -}}
{{- range $.AllTranslations -}}
{{- if eq .Language.Lang $lang.Lang -}}{{- $target = .RelPermalink -}}{{- end -}}
{{- end -}}
{{- if not $target -}}{{- $target = ($lang.Lang | printf "/%s/" | relURL) -}}{{- end -}}
<a class="lang" href="{{ $target }}" lang="{{ $lang.LanguageCode }}">{{ $lang.LanguageName }}</a>
{{- end -}}
</li>
{{- end -}}
{{- end -}}
</span>
</ul>
</details>

View file

@ -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