feat: use i18n
This commit is contained in:
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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
<script src="./telegram-web-app.js"></script>
|
||||||
<link rel="stylesheet" href="./subscription_webapp.css">
|
<link rel="stylesheet" href="./subscription_webapp.css">
|
||||||
</head>
|
</head>
|
||||||
@@ -28,6 +36,16 @@
|
|||||||
<div id="brand-title" class="brand-title" data-brand-title>Моя подписка</div>
|
<div id="brand-title" class="brand-title" data-brand-title>Моя подписка</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="app-header-actions">
|
<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">
|
<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()">
|
<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">
|
<img id="user-chip-avatar" class="user-chip-avatar" alt="" aria-hidden="true">
|
||||||
|
|||||||
@@ -109,9 +109,19 @@ const MOCK = (() => {
|
|||||||
telegramLinkInProgress: false,
|
telegramLinkInProgress: false,
|
||||||
toastTimer: null,
|
toastTimer: null,
|
||||||
userMenuOpen: false,
|
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 = {
|
const TW = {
|
||||||
iconBtn: 'icon-btn',
|
iconBtn: 'icon-btn',
|
||||||
btnBase: 'btn',
|
btnBase: 'btn',
|
||||||
@@ -459,6 +469,8 @@ const MOCK = (() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', handleDocumentClickForUserMenu);
|
document.addEventListener('click', handleDocumentClickForUserMenu);
|
||||||
|
document.addEventListener('click', handleDocumentClickForLangMenu);
|
||||||
|
renderLangMenu();
|
||||||
|
|
||||||
boot();
|
boot();
|
||||||
|
|
||||||
@@ -1211,6 +1223,64 @@ const MOCK = (() => {
|
|||||||
if (event.key === 'Escape' && state.userMenuOpen) {
|
if (event.key === 'Escape' && state.userMenuOpen) {
|
||||||
toggleUserMenu(false);
|
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() {
|
function openPromoModal() {
|
||||||
@@ -2253,13 +2323,39 @@ const MOCK = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLanguage() {
|
function getLanguage() {
|
||||||
const raw = (
|
const candidates = [];
|
||||||
state.data && state.data.user && state.data.user.language_code
|
if (state.langOverride) candidates.push(state.langOverride);
|
||||||
? state.data.user.language_code
|
if (state.data && state.data.user && state.data.user.language_code) {
|
||||||
: (CFG.language || document.documentElement.lang || 'ru')
|
candidates.push(state.data.user.language_code);
|
||||||
).toLowerCase();
|
}
|
||||||
const short = raw.split('-')[0];
|
const tgUser = tg && tg.initDataUnsafe && tg.initDataUnsafe.user;
|
||||||
return I18N[short] ? short : 'ru';
|
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 = {}) {
|
function t(key, params = {}) {
|
||||||
|
|||||||
@@ -180,6 +180,66 @@
|
|||||||
@apply relative inline-flex;
|
@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 {
|
.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)];
|
@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)];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user