Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/litellm_rtksync/i18n.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"""Internacionalização da interface do painel.

Idioma padrão: inglês. Português e espanhol são opcionais e escolhidos pelo
seletor de bandeiras no topo do painel e no rodapé do login. A escolha é
Idioma padrão: português (pt-br). Inglês e espanhol são opcionais e escolhidos
pelo seletor de bandeiras no topo do painel e no cabeçalho do login. A escolha é
persistida em SQLite (ver prefs.py), em cookies e em localStorage/sessionStorage,
sobrevivendo a troca de navegador, reinicializações e limpezas de sessão.

Os catálogos de tradução ficam armazenados em arquivos JSON em `locales/*.json`,
sendo carregados dinamicamente na inicialização e sob demanda via
`recarrega_traducoes()`.

Chave ausente numa tradução cai para o inglês, nunca para a chave crua.
Chave ausente numa tradução cai para o padrão, nunca para a chave crua.

O catálogo é o mesmo texto nos três irmãos. O nome do produto e o do gateway
nunca são escritos aqui: entram por interpolação a partir de `identidade.py`,
Expand All @@ -22,12 +22,12 @@

from .identidade import ROTULOS_DO_PRODUTO, NOME_DO_GATEWAY

DEFAULT_LANGUAGE = "en"
DEFAULT_LANGUAGE = "pt"

# Código do idioma -> (rótulo nativo, classe de bandeira do flag-icons)
LANGUAGES: Dict[str, tuple] = {
"en": ("English", "fi-us"),
"pt": ("Português", "fi-br"),
"en": ("English", "fi-us"),
"es": ("Español", "fi-es"),
}

Expand Down Expand Up @@ -79,7 +79,7 @@ def normalize_language(code: str) -> str:


def translate(key: str, lang: str = DEFAULT_LANGUAGE, **params) -> str:
"""Traduz uma chave, com fallback para inglês e interpolação opcional."""
"""Traduz uma chave, com fallback para o idioma padrão e interpolação opcional."""
lang = normalize_language(lang)
# O rótulo do produto vem primeiro: são as poucas chaves que dependem do que
# ESTE gateway faz, e elas moram em identidade.py justamente para que o
Expand All @@ -90,7 +90,11 @@ def translate(key: str, lang: str = DEFAULT_LANGUAGE, **params) -> str:
if text is None:
text = ROTULOS_DO_PRODUTO.get(DEFAULT_LANGUAGE, {}).get(key)
if text is None:
text = TRANSLATIONS.get(DEFAULT_LANGUAGE, {}).get(key, key)
text = TRANSLATIONS.get(DEFAULT_LANGUAGE, {}).get(key)
if text is None:
text = ROTULOS_DO_PRODUTO.get("en", {}).get(key)
if text is None:
text = TRANSLATIONS.get("en", {}).get(key, key)
if params:
try:
return text.format(**params)
Expand Down
37 changes: 28 additions & 9 deletions src/litellm_rtksync/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def script_de_idioma() -> str:
<script>
(function() {
try {
var salvo = localStorage.getItem('rtksync_lang') || sessionStorage.getItem('rtksync_lang');
var salvo = sessionStorage.getItem('rtksync_lang') || localStorage.getItem('rtksync_lang');
var atual = document.documentElement.lang;
if (salvo && ['en', 'pt', 'es'].indexOf(salvo) !== -1) {
document.cookie = 'rtksync_lang=' + encodeURIComponent(salvo) + '; path=/; max-age=31536000; SameSite=Lax';
Expand All @@ -210,8 +210,8 @@ def script_de_idioma() -> str:
})();
function setRtksyncLang(lang) {
try {
localStorage.setItem('rtksync_lang', lang);
sessionStorage.setItem('rtksync_lang', lang);
localStorage.setItem('rtksync_lang', lang);
document.cookie = 'rtksync_lang=' + encodeURIComponent(lang) + '; path=/; max-age=31536000; SameSite=Lax';
} catch(e) {}
var u = new URL(window.location.href);
Expand Down Expand Up @@ -584,16 +584,34 @@ def render_login_page(
.form-control {{ background: var(--bg); border-color: var(--line); color: var(--text); }}
.form-control:focus {{ background: var(--bg); color: var(--text);
border-color: var(--accent); box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--accent) 25%, transparent); }}
.barra-acoes {{ display: flex; align-items: center; gap: .5rem; }}
.barra-acoes .btn {{ height: 2rem; padding: 0 .65rem; display: inline-flex; align-items: center; }}
.dropdown-menu {{ background: var(--surface); border: 1px solid var(--line); }}
.dropdown-item {{ color: var(--text); }}
.dropdown-item:hover, .dropdown-item:focus {{ background: var(--surface-2); color: var(--text); }}
.dropdown-item.active {{ background: var(--accent); color: var(--bg); }}
</style>
</head>
<body class="d-flex flex-column min-vh-100 justify-content-between">
<header class="d-flex align-items-center justify-content-between px-3 px-sm-4 py-3 border-bottom" style="border-color: var(--line) !important;">
<div class="d-flex align-items-center gap-3">
<span class="brand-mark"><i class="bi {ICONE_DO_PRODUTO}" aria-hidden="true"></i></span>
<div>
<h1 class="h5 mb-0 fw-bold">{NOME_DO_PRODUTO}</h1>
<span class="text-secondary small font-monospace">{NOME_DO_GATEWAY}</span>
</div>
</div>
<div class="barra-acoes">
{render_language_switcher(lang)}
</div>
</header>
<div class="flex-grow-1 d-flex align-items-center justify-content-center p-3">
<main class="card shadow-sm" style="max-width:26rem;width:100%">
<div class="card-body p-4">
<div class="d-flex align-items-center gap-3 mb-3">
<span class="brand-mark"><i class="bi {ICONE_DO_PRODUTO}" aria-hidden="true"></i></span>
<div>
<h1 class="h5 mb-0 fw-bold">{NOME_DO_PRODUTO}</h1>
<h2 class="h5 mb-0 fw-bold">{NOME_DO_PRODUTO}</h2>
<span class="text-secondary small font-monospace">{NOME_DO_GATEWAY}</span>
</div>
</div>
Expand All @@ -605,7 +623,8 @@ def render_login_page(
</div>
</main>
</div>
{rodape_da_pathbit(lang, com_seletor=True)}
{rodape_da_pathbit(lang, com_seletor=False)}
<script src="{BOOTSTRAP_JS}"></script>
</body>
</html>""".encode("utf-8")

Expand All @@ -628,20 +647,20 @@ def render_language_switcher(current: str) -> str:
for code, (label, flag) in LANGUAGES.items():
active = " active" if code == current else ""
items.append(
f'<li><button class="dropdown-item d-flex align-items-center gap-2{active}" '
f'type="submit" name="lang" value="{esc(code)}" onclick="setRtksyncLang(\'{esc(code)}\')">'
f'<span class="fi {esc(flag)}"></span>{esc(label)}</button></li>'
f'<li><a class="dropdown-item d-flex align-items-center gap-2{active}" '
f'href="?lang={esc(code)}" onclick="setRtksyncLang(\'{esc(code)}\'); return false;">'
f'<span class="fi {esc(flag)}"></span>{esc(label)}</a></li>'
)
return f"""
<form method="post" action="/acoes/idioma" class="m-0 dropdown">
<div class="dropdown m-0">
<button class="btn btn-outline-light btn-sm dropdown-toggle d-inline-flex align-items-center gap-2"
type="button" data-bs-toggle="dropdown" aria-expanded="false"
aria-label="{esc(translate('language.label', current))}">
<span class="fi {esc(current_flag)}"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">{"".join(items)}
</ul>
</form>"""
</div>"""


def metric_card(label: str, value: Any, icon: str, tone: str) -> str:
Expand Down
15 changes: 15 additions & 0 deletions src/litellm_rtksync/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,21 @@ def respond_html(self, corpo: bytes, status: HTTPStatus = HTTPStatus.OK) -> None
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(corpo)))
self.send_header("Cache-Control", "no-store")
query = parse_qs(urlparse(self.path).query)
if "lang" in query and query["lang"]:
lang = normalize_language(query["lang"][0])
if lang in LANGUAGES:
seguro = self.eh_conexao_segura()
s = "; Secure" if seguro else ""
self.send_header(
"Set-Cookie",
f"rtksync_lang={lang}; Path=/; Max-Age=31536000; SameSite=Lax{s}",
)
if self.settings and getattr(self, "authenticated_user", None):
try:
set_preference(self.prefs_path(), "language", lang)
except Exception:
pass
self.end_headers()
self.write_body(corpo)

Expand Down
Loading