i18n: style the settings select + add a topbar lang toggle

Two issues addressed:

1. The /settings language <select> was unstyled — .settings-select and
   .settings-status classes didn't exist, so the dropdown rendered
   with full native browser chrome and clashed visually with the rest
   of the panel. Added a terminal-aesthetic select: transparent
   background, 1px var(--border), custom chevron via crossed
   linear-gradients, accent border on focus/hover. Disabled options
   (ES/FR/DE 'coming soon') render in --dim.

2. Added a compact EN | IT pill in the topbar next to the theme
   toggle, mirroring the .tone-toggle visual rhythm. Shown only when
   a user is signed in (admins skipped). Optimistic UI: clicking
   flips the pill immediately, PATCHes /api/settings/language, and
   reverts on failure. On /log specifically the page reloads so the
   user sees the localized version of the strategic log right away.

The /settings dropdown still surfaces all five languages (with ES/FR/DE
disabled) for visibility; the topbar pill keeps to the two active
languages to stay compact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-27 18:14:23 +02:00
parent 50ac6b9366
commit fb71854238
2 changed files with 107 additions and 0 deletions

View file

@ -136,6 +136,35 @@ a:hover { text-decoration: underline; }
color: var(--bg); color: var(--bg);
} }
/* Language toggle in the topbar same visual rhythm as the tone
* toggle so the two controls read as a pair. Only EN and IT are
* visible here; the WIP languages (ES/FR/DE) live in /settings. */
.lang-toggle {
display: inline-flex;
border: 1px solid var(--border);
font-family: var(--font-mono);
font-size: 10.5px;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.lang-toggle button {
background: transparent;
color: var(--muted);
border: 0;
padding: 4px 8px;
cursor: pointer;
font: inherit;
letter-spacing: inherit;
text-transform: inherit;
}
.lang-toggle button + button { border-left: 1px solid var(--border); }
.lang-toggle button:hover { color: var(--accent); }
.lang-toggle[data-lang="en"] button[data-value="en"],
.lang-toggle[data-lang="it"] button[data-value="it"] {
background: var(--accent);
color: var(--bg);
}
.app-main { .app-main {
padding: 14px; padding: 14px;
display: grid; display: grid;
@ -1020,6 +1049,46 @@ details[open] .pf-analysis__head-left::before { content: "▾ "; }
margin-left: 8px; margin-left: 8px;
} }
/* Terminal-aesthetic <select> used in the Settings page. Native
* browser chrome stripped; we render a small chevron via crossed
* linear-gradients so the control matches the rest of the panel. */
.settings-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: transparent;
border: 1px solid var(--border);
color: var(--text);
padding: 4px 28px 4px 8px;
font-family: var(--font-mono);
font-size: 12px;
border-radius: 2px;
cursor: pointer;
background-image:
linear-gradient(45deg, transparent 50%, var(--dim) 50%),
linear-gradient(-45deg, transparent 50%, var(--dim) 50%);
background-position: calc(100% - 13px) 50%, calc(100% - 9px) 50%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
transition: border-color 120ms ease-out, color 120ms ease-out;
}
.settings-select:hover,
.settings-select:focus {
outline: none;
border-color: var(--accent);
color: var(--text);
}
.settings-select option { color: var(--text); background: var(--surface); }
.settings-select option:disabled { color: var(--dim); }
.settings-status {
font-family: var(--font-mono);
font-size: 11px;
color: var(--muted);
letter-spacing: 0.04em;
}
.settings-status:empty { display: none; }
/* Sections are <details> elements collapsed by default to keep the /* Sections are <details> elements collapsed by default to keep the
settings page scannable. Click the summary to expand. */ settings page scannable. Click the summary to expand. */
.settings-section { .settings-section {

View file

@ -134,6 +134,34 @@
if (el && window.htmx) window.htmx.trigger(el, 'tone-changed'); if (el && window.htmx) window.htmx.trigger(el, 'tone-changed');
}); });
}; };
window.cassandraSetLang = async function (newLang) {
var pill = document.getElementById('lang-toggle');
if (!pill) return;
var prev = pill.dataset.lang;
if (prev === newLang) return;
// Optimistic update — flip the pill immediately so the click feels
// responsive. Revert on PATCH failure.
pill.dataset.lang = newLang;
try {
var r = await fetch('/api/settings/language', {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
credentials: 'same-origin',
body: JSON.stringify({lang: newLang}),
});
if (!r.ok) throw new Error('HTTP ' + r.status);
// Reload localized panels so the user immediately sees content
// in the new language (strategic log, dashboard header, etc.).
if (window.location.pathname === '/log' ||
window.location.pathname.startsWith('/log/')) {
window.location.reload();
}
} catch (e) {
pill.dataset.lang = prev;
console.warn('language switch failed:', e);
}
};
</script> </script>
<script> <script>
// Render any <time datetime="..."> in the browser's local timezone. // Render any <time datetime="..."> in the browser's local timezone.
@ -180,6 +208,16 @@
<span class="theme-toggle__label"></span> <span class="theme-toggle__label"></span>
</button> </button>
{% set cu = request.state.current_user if request.state and request.state.current_user is defined else None %} {% set cu = request.state.current_user if request.state and request.state.current_user is defined else None %}
{% if cu and cu.user %}
<div id="lang-toggle" class="lang-toggle" data-lang="{{ cu.user.lang or 'en' }}"
role="group" aria-label="AI output language"
title="Language the AI uses for the log, digest and portfolio commentary">
<button type="button" data-value="en"
onclick="cassandraSetLang('en')">EN</button>
<button type="button" data-value="it"
onclick="cassandraSetLang('it')">IT</button>
</div>
{% endif %}
{% if cu and (cu.user or cu.is_admin) %} {% if cu and (cu.user or cu.is_admin) %}
<div class="user-menu"> <div class="user-menu">
<button type="button" id="user-menu-toggle" class="user-chip" <button type="button" id="user-menu-toggle" class="user-chip"