Files
memos-bber/js/i18n.js
T
jonny ed5fc86e39 Replace language select with toggle menu
Replace the old <select> language picker with a custom toggle button and dropdown menu. Updates include new popup HTML (langToggle button, langMenu with menuitemradio buttons), new CSS for .lang-toggle/.lang-menu/.lang-menu-item (styling, hover/active states, shadow, rounded corners), and JS to manage label sync, menu state, click handlers, outside-click and Escape to close, and applying the selected UI language. Also update references in i18n code (setTitle target and language init flow) and persist language selection. Bump manifest version from 2026.03.24 to 2026.03.25.
2026-03-23 11:29:49 +08:00

205 lines
6.0 KiB
JavaScript

const UI_LANGUAGE_STORAGE_KEY = 'uiLanguage'
const SUPPORTED_UI_LANGUAGES = new Set(['auto', 'en', 'zh_CN', 'ja', 'ko'])
function normalizeUiLanguage(value) {
const lang = String(value || 'auto')
return SUPPORTED_UI_LANGUAGES.has(lang) ? lang : 'auto'
}
function storageSyncGet(defaults) {
return new Promise((resolve) => {
chrome.storage.sync.get(defaults, (items) => resolve(items || {}))
})
}
function storageSyncSet(items) {
return new Promise((resolve) => {
chrome.storage.sync.set(items, () => resolve())
})
}
async function loadLocaleMessages(locale) {
if (!locale || locale === 'auto') return null
try {
const url = chrome.runtime.getURL(`_locales/${locale}/messages.json`)
const resp = await fetch(url)
if (!resp.ok) return null
return await resp.json()
} catch (_) {
return null
}
}
function formatSubstitutions(message, substitutions) {
if (!message) return ''
if (substitutions == null) return message
const subs = Array.isArray(substitutions) ? substitutions : [substitutions]
let out = message
for (let i = 0; i < subs.length; i++) {
const v = String(subs[i])
out = out.replaceAll(`$${i + 1}`, v)
out = out.replace('%s', v)
}
return out
}
let currentUiLanguage = 'auto'
let overrideMessages = null
function getLanguageToggleLabel(lang) {
if (lang === 'en') return 'EN'
if (lang === 'zh_CN') return '中'
if (lang === 'ja') return '日'
if (lang === 'ko') return '한'
return 'A'
}
function syncLanguageToggleText(lang) {
const text = document.getElementById('langToggleText')
if (text) text.textContent = getLanguageToggleLabel(lang)
}
function syncLanguageMenuState(lang) {
const items = document.querySelectorAll('.lang-menu-item')
items.forEach((item) => {
const isActive = item.getAttribute('data-lang') === lang
item.classList.toggle('active', isActive)
item.setAttribute('aria-checked', isActive ? 'true' : 'false')
})
}
function setLanguageMenuOpen(isOpen) {
const toggle = document.getElementById('langToggle')
const menu = document.getElementById('langMenu')
if (!toggle || !menu) return
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false')
menu.classList.toggle('hidden', !isOpen)
}
function t(key, substitutions) {
const msg = overrideMessages && overrideMessages[key] && overrideMessages[key].message
if (typeof msg === 'string' && msg.length > 0) {
return formatSubstitutions(msg, substitutions)
}
const chromeMsg = chrome.i18n.getMessage(key, substitutions) || ''
return formatSubstitutions(chromeMsg, substitutions)
}
function setText(id, messageKey) {
const el = document.getElementById(id)
if (el) el.textContent = t(messageKey)
}
function setPlaceholder(id, messageKey) {
const el = document.getElementById(id)
if (el) el.placeholder = t(messageKey)
}
function setTitle(id, messageKey) {
const el = document.getElementById(id)
if (el) el.title = t(messageKey)
}
function applyStaticI18n() {
setText('saveKey', 'saveBtn')
setText('saveTag', 'saveBtn')
setText('supportedMemosVersion', 'supportedMemosVersion')
setPlaceholder('apiUrl', 'placeApiUrl')
setPlaceholder('apiTokens', 'placeApiTokens')
setPlaceholder('content', 'placeContent')
setText('lockPrivate', 'lockPrivate')
setText('lockProtected', 'lockProtected')
setText('lockPublic', 'lockPublic')
setText('content_submit_text', 'submitBtn')
const fullscreen = document.getElementById('fullscreen')
if (fullscreen) fullscreen.setAttribute('aria-label', t('tipFullscreen'))
setPlaceholder('hideInput', 'placeHideInput')
setPlaceholder('showInput', 'placeShowInput')
setText('uploadlist-title', 'uploadedListTitle')
// Language switcher
setText('langOptionAuto', 'langAuto')
setText('langOptionEn', 'langEnglish')
setText('langOptionZhCN', 'langChineseSimplified')
setText('langOptionJa', 'langJapanese')
setText('langOptionKo', 'langKorean')
setTitle('langToggle', 'tipLanguage')
const langToggle = document.getElementById('langToggle')
if (langToggle) langToggle.setAttribute('aria-label', t('tipLanguage'))
// Native hover tooltips (title)
setTitle('opensite', 'tipOpenSite')
setTitle('blog_info_edit', 'tipSettings')
setTitle('tags', 'tipTags')
setTitle('newtodo', 'tipTodo')
setTitle('upres', 'tipUpload')
setTitle('getlink', 'tipLink')
setTitle('random', 'tipRandom')
setTitle('search', 'tipSearch')
setTitle('lock', 'tipVisibility')
setTitle('content_submit_text', 'tipSend')
setTitle('fullscreen', 'tipFullscreen')
setTitle('editor-resize-handle', 'tipResize')
}
async function setUiLanguage(nextLang, { persist = true } = {}) {
const lang = normalizeUiLanguage(nextLang)
currentUiLanguage = lang
overrideMessages = await loadLocaleMessages(lang)
applyStaticI18n()
syncLanguageToggleText(lang)
syncLanguageMenuState(lang)
if (persist) await storageSyncSet({ [UI_LANGUAGE_STORAGE_KEY]: lang })
window.dispatchEvent(new CustomEvent('i18n:changed', { detail: { lang } }))
}
async function initLanguageSwitcher() {
const switcher = document.getElementById('lang_switcher')
const toggle = document.getElementById('langToggle')
const langItems = document.querySelectorAll('.lang-menu-item')
if (toggle) {
toggle.addEventListener('click', (event) => {
event.stopPropagation()
const isOpen = toggle.getAttribute('aria-expanded') === 'true'
setLanguageMenuOpen(!isOpen)
})
}
langItems.forEach((item) => {
item.addEventListener('click', async (event) => {
event.stopPropagation()
setLanguageMenuOpen(false)
await setUiLanguage(item.getAttribute('data-lang'))
})
})
document.addEventListener('click', (event) => {
if (!switcher || switcher.contains(event.target)) return
setLanguageMenuOpen(false)
})
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') setLanguageMenuOpen(false)
})
const storedItems = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
const stored = normalizeUiLanguage(storedItems[UI_LANGUAGE_STORAGE_KEY])
await setUiLanguage(stored, { persist: false })
setLanguageMenuOpen(false)
}
window.t = t
window.setUiLanguage = setUiLanguage
window.getUiLanguage = () => currentUiLanguage
applyStaticI18n()
window.i18nReady = initLanguageSwitcher()