Lang menu and autodetect
This commit is contained in:
parent
1b439e9499
commit
93574abc58
6 changed files with 128 additions and 18 deletions
|
|
@ -71,9 +71,55 @@ a { color: var(--green); }
|
||||||
}
|
}
|
||||||
.site-nav a { color: #fff; text-decoration: none; opacity: .95; white-space: nowrap; }
|
.site-nav a { color: #fff; text-decoration: none; opacity: .95; white-space: nowrap; }
|
||||||
.site-nav a:hover { text-decoration: underline; }
|
.site-nav a:hover { text-decoration: underline; }
|
||||||
.lang-switch { display: inline-flex; gap: .6rem; font-size: .92rem; white-space: nowrap; }
|
/* A typical globe+code language menu (<details>/<summary>, no JS) instead of
|
||||||
.lang-switch .current { opacity: .7; }
|
spelling out every language name in the header — scales past 3 languages
|
||||||
.lang-switch a.lang { color: #fff; }
|
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. */
|
/* Narrow phones: brand on its own row, nav wraps beneath it, left-aligned. */
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
"navHome": {
|
"navHome": {
|
||||||
"other": "Home"
|
"other": "Home"
|
||||||
},
|
},
|
||||||
|
"navLanguage": {
|
||||||
|
"other": "Language"
|
||||||
|
},
|
||||||
"screenshotsTitle": {
|
"screenshotsTitle": {
|
||||||
"other": "A look inside"
|
"other": "A look inside"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
"navHome": {
|
"navHome": {
|
||||||
"other": "Inicio"
|
"other": "Inicio"
|
||||||
},
|
},
|
||||||
|
"navLanguage": {
|
||||||
|
"other": "Idioma"
|
||||||
|
},
|
||||||
"screenshotsTitle": {
|
"screenshotsTitle": {
|
||||||
"other": "Un vistazo por dentro"
|
"other": "Un vistazo por dentro"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
"navHome": {
|
"navHome": {
|
||||||
"other": "Início"
|
"other": "Início"
|
||||||
},
|
},
|
||||||
|
"navLanguage": {
|
||||||
|
"other": "Idioma"
|
||||||
|
},
|
||||||
"screenshotsTitle": {
|
"screenshotsTitle": {
|
||||||
"other": "Um vislumbre por dentro"
|
"other": "Um vislumbre por dentro"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,34 @@
|
||||||
{{- /* Links to this page in the other language; falls back to that language's home. */ -}}
|
{{- /*
|
||||||
<span class="lang-switch">
|
A typical "globe + code" language menu, done with <details>/<summary> so it
|
||||||
{{- range .Site.Languages -}}
|
needs no JS (site stays static per config.toml). Links to this page in the
|
||||||
{{- $lang := . -}}
|
other language; falls back to that language's home.
|
||||||
{{- if eq $lang.Lang $.Site.Language.Lang -}}
|
*/ -}}
|
||||||
<span class="lang current" aria-current="true">{{ $lang.LanguageName }}</span>
|
<details class="lang-switch">
|
||||||
{{- else -}}
|
<summary aria-label="{{ T "navLanguage" }}">
|
||||||
{{- $target := "" -}}
|
{{- /* Material Icons "translate" glyph (文A) — the same icon the app uses
|
||||||
{{- range $.AllTranslations -}}
|
for its language picker (Icons.translate in settings_screen.dart).
|
||||||
{{- if eq .Language.Lang $lang.Lang -}}{{- $target = .RelPermalink -}}{{- end -}}
|
Inline so it needs no icon font or external request (self-contained
|
||||||
{{- end -}}
|
per CSP). */ -}}
|
||||||
{{- if not $target -}}{{- $target = ($lang.Lang | printf "/%s/" | relURL) -}}{{- end -}}
|
<svg class="lang-globe" aria-hidden="true" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
||||||
<a class="lang" href="{{ $target }}" lang="{{ $lang.LanguageCode }}">{{ $lang.LanguageName }}</a>
|
<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 -}}
|
||||||
{{- end -}}
|
</ul>
|
||||||
</span>
|
</details>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.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
|
# Security headers. The page is fully static and self-contained: inline CSS
|
||||||
# (needs style-src 'unsafe-inline'), same-origin images (+ data: URIs), no
|
# (needs style-src 'unsafe-inline'), same-origin images (+ data: URIs), no
|
||||||
# third-party scripts. HSTS is host-scoped (no includeSubDomains) so it can't
|
# third-party scripts. HSTS is host-scoped (no includeSubDomains) so it can't
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue