feat: use i18n

This commit is contained in:
3252a8
2026-04-26 12:34:16 +03:00
parent 9c499fe3c2
commit 94b0787cad
4 changed files with 183 additions and 9 deletions
File diff suppressed because one or more lines are too long
@@ -11,6 +11,14 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
@font-face {
font-family: 'Twemoji Country Flags';
unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;
src: url('https://cdn.jsdelivr.net/npm/country-flag-emoji-polyfill@0.1/dist/TwemojiCountryFlags.woff2') format('woff2');
font-display: swap;
}
</style>
<script src="./telegram-web-app.js"></script>
<link rel="stylesheet" href="./subscription_webapp.css">
</head>
@@ -28,6 +36,16 @@
<div id="brand-title" class="brand-title" data-brand-title>Моя подписка</div>
</div>
<div class="app-header-actions">
<div class="lang-menu">
<button id="lang-chip" class="lang-chip" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="lang-dropdown" aria-label="Language" onclick="toggleLangMenu()">
<span id="lang-chip-flag" class="lang-chip-flag" aria-hidden="true">🇷🇺</span>
<span id="lang-chip-label" class="lang-chip-label">RU</span>
<svg class="lang-chip-caret" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true">
<path d="M297.4 470.6C309.9 483.1 330.2 483.1 342.7 470.6L534.7 278.6C547.2 266.1 547.2 245.8 534.7 233.3C522.2 220.8 501.9 220.8 489.4 233.3L320 402.7L150.6 233.4C138.1 220.9 117.8 220.9 105.3 233.4C92.8 245.9 92.8 266.2 105.3 278.7L297.3 470.7z"/>
</svg>
</button>
<div id="lang-dropdown" class="lang-dropdown hidden" role="menu"></div>
</div>
<div class="user-menu">
<button id="user-chip" class="user-chip" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="user-dropdown" onclick="toggleUserMenu()">
<img id="user-chip-avatar" class="user-chip-avatar" alt="" aria-hidden="true">
+104 -8
View File
@@ -109,9 +109,19 @@ const MOCK = (() => {
telegramLinkInProgress: false,
toastTimer: null,
userMenuOpen: false,
userMenuCloseTimer: null
userMenuCloseTimer: null,
langOverride: (function() {
try { return localStorage.getItem('rw_webapp_lang') || ''; } catch (e) { return ''; }
})(),
langMenuOpen: false,
langMenuCloseTimer: null
};
const LANG_OPTIONS = [
{ code: 'ru', label: 'Русский', short: 'RU', flag: '🇷🇺' },
{ code: 'en', label: 'English', short: 'EN', flag: '🇬🇧' }
];
const TW = {
iconBtn: 'icon-btn',
btnBase: 'btn',
@@ -459,6 +469,8 @@ const MOCK = (() => {
});
document.addEventListener('click', handleDocumentClickForUserMenu);
document.addEventListener('click', handleDocumentClickForLangMenu);
renderLangMenu();
boot();
@@ -1211,6 +1223,64 @@ const MOCK = (() => {
if (event.key === 'Escape' && state.userMenuOpen) {
toggleUserMenu(false);
}
if (event.key === 'Escape' && state.langMenuOpen) {
toggleLangMenu(false);
}
}
function renderLangMenu() {
const current = getLanguage();
const chipLabel = document.getElementById('lang-chip-label');
const chipFlag = document.getElementById('lang-chip-flag');
const opt = LANG_OPTIONS.find(o => o.code === current) || LANG_OPTIONS[0];
if (chipLabel) chipLabel.textContent = opt.short;
if (chipFlag) chipFlag.textContent = opt.flag;
const list = document.getElementById('lang-dropdown');
if (!list) return;
list.innerHTML = LANG_OPTIONS.map(o => (
'<button type="button" role="menuitem" class="lang-dropdown-item' +
(o.code === current ? ' is-active' : '') +
'" data-lang="' + o.code + '">' +
'<span class="lang-dropdown-flag" aria-hidden="true">' + o.flag + '</span>' +
'<span class="lang-dropdown-label">' + escapeHtml(o.label) + '</span>' +
'</button>'
)).join('');
list.querySelectorAll('[data-lang]').forEach(btn => {
btn.addEventListener('click', () => setLanguage(btn.dataset.lang));
});
}
function toggleLangMenu(force) {
const open = typeof force === 'boolean' ? force : !state.langMenuOpen;
state.langMenuOpen = open;
const chip = document.getElementById('lang-chip');
const dropdown = document.getElementById('lang-dropdown');
if (!chip || !dropdown) return;
if (state.langMenuCloseTimer) {
window.clearTimeout(state.langMenuCloseTimer);
state.langMenuCloseTimer = null;
}
chip.setAttribute('aria-expanded', open ? 'true' : 'false');
if (open) {
dropdown.classList.remove('hidden');
dropdown.classList.remove('show');
window.requestAnimationFrame(() => {
if (state.langMenuOpen) dropdown.classList.add('show');
});
return;
}
dropdown.classList.remove('show');
state.langMenuCloseTimer = window.setTimeout(() => {
if (!state.langMenuOpen) dropdown.classList.add('hidden');
state.langMenuCloseTimer = null;
}, 180);
}
function handleDocumentClickForLangMenu(event) {
if (!state.langMenuOpen) return;
const menu = document.querySelector('.lang-menu');
if (menu && menu.contains(event.target)) return;
toggleLangMenu(false);
}
function openPromoModal() {
@@ -2253,13 +2323,39 @@ const MOCK = (() => {
}
function getLanguage() {
const raw = (
state.data && state.data.user && state.data.user.language_code
? state.data.user.language_code
: (CFG.language || document.documentElement.lang || 'ru')
).toLowerCase();
const short = raw.split('-')[0];
return I18N[short] ? short : 'ru';
const candidates = [];
if (state.langOverride) candidates.push(state.langOverride);
if (state.data && state.data.user && state.data.user.language_code) {
candidates.push(state.data.user.language_code);
}
const tgUser = tg && tg.initDataUnsafe && tg.initDataUnsafe.user;
if (tgUser && tgUser.language_code) candidates.push(tgUser.language_code);
if (typeof navigator !== 'undefined') {
if (Array.isArray(navigator.languages)) {
navigator.languages.forEach(l => l && candidates.push(l));
}
if (navigator.language) candidates.push(navigator.language);
}
if (CFG.language) candidates.push(CFG.language);
if (document.documentElement.lang) candidates.push(document.documentElement.lang);
for (const raw of candidates) {
const short = String(raw).toLowerCase().split('-')[0];
if (I18N[short]) return short;
}
return 'ru';
}
function setLanguage(lang) {
const short = String(lang || '').toLowerCase().split('-')[0];
if (!I18N[short]) return;
state.langOverride = short;
try { localStorage.setItem('rw_webapp_lang', short); } catch (e) { }
applyI18n();
if (state.data) {
try { render(); } catch (e) { }
}
renderLangMenu();
toggleLangMenu(false);
}
function t(key, params = {}) {
@@ -180,6 +180,66 @@
@apply relative inline-flex;
}
.lang-menu {
@apply relative inline-flex;
}
.lang-chip {
@apply inline-flex min-h-[38px] cursor-pointer items-center gap-1.5 rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.03)] px-2 text-left font-extrabold text-[12px] text-[var(--text-primary)] no-underline transition-[transform,border-color,background,color,box-shadow] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] hover:border-[color-mix(in_srgb,var(--accent)_42%,var(--border))] hover:bg-[var(--bg-card-hover)];
}
.lang-chip[aria-expanded="true"] {
@apply border-[color-mix(in_srgb,var(--accent)_55%,var(--border))] bg-[var(--bg-card-hover)];
}
.lang-chip-flag {
@apply text-[14px] leading-none;
font-family: 'Twemoji Country Flags', 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
}
.lang-chip-label {
@apply font-[family-name:var(--font-mono)] text-[11px] tracking-wide;
}
.lang-chip-caret {
@apply h-3 w-3 flex-none fill-[var(--text-secondary)] transition-transform;
}
.lang-chip[aria-expanded="true"] .lang-chip-caret {
transform: rotate(180deg);
}
.lang-dropdown {
@apply absolute right-0 top-[calc(100%+8px)] z-30 grid w-[160px] origin-top-right gap-0 rounded-[var(--radius-md)] border border-[var(--border)] bg-[var(--bg-card)] opacity-0 shadow-[0_24px_48px_rgba(0,0,0,0.45)] transition-[opacity,transform] duration-200 ease-[cubic-bezier(0.2,0.8,0.2,1)] pointer-events-none -translate-y-2 scale-[0.96];
backdrop-filter: blur(14px);
will-change: opacity, transform;
}
.lang-dropdown.show {
@apply opacity-100 pointer-events-auto translate-y-0 scale-100;
}
.lang-dropdown-item {
@apply flex cursor-pointer items-center gap-2.5 border-0 bg-transparent px-3 py-2.5 text-left text-[13px] font-bold text-[var(--text-secondary)] transition-colors hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-primary)];
}
.lang-dropdown-item + .lang-dropdown-item {
@apply border-t border-[var(--border)];
}
.lang-dropdown-item.is-active {
@apply text-[var(--accent)];
}
.lang-dropdown-flag {
@apply text-[16px] leading-none;
font-family: 'Twemoji Country Flags', 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
}
.lang-dropdown-label {
@apply flex-1 overflow-hidden text-ellipsis whitespace-nowrap;
}
.user-chip {
@apply inline-flex min-h-[38px] min-w-0 max-w-[200px] cursor-pointer items-center gap-2 rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.03)] py-1 pl-1 pr-2.5 text-left font-extrabold text-[13px] text-[var(--text-primary)] no-underline transition-[transform,border-color,background,color,box-shadow] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] hover:border-[color-mix(in_srgb,var(--accent)_42%,var(--border))] hover:bg-[var(--bg-card-hover)];
}