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.
This commit is contained in:
jonny
2026-03-23 11:29:49 +08:00
parent 0f468d49aa
commit ed5fc86e39
5 changed files with 149 additions and 24 deletions
+1
View File
@@ -5,6 +5,7 @@ Chrome 应用商店:<https://chrome.google.com/webstore/detail/memos-bber/cbhj
一个通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
## 更新日志
- 20260325 优化语言按钮样式
- 20260323 优化中文显示效果
- 20260322 适配移动端竖屏窗口
- 20260310 记忆拖拽窗口大小,移除拖拽窗口动画
+67 -5
View File
@@ -322,17 +322,79 @@ input.inputer{border-bottom: 1px solid #ccc;width:75%;}
top: .55rem;
}
.lang-select{
border: 1px solid rgb(229,231,235);
border-radius: .25rem;
background-color: rgb(255,255,255);
.lang-toggle{
border: none;
border-radius: 0;
background-color: transparent;
color: #666;
min-width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-sizing: border-box;
padding: 0;
opacity: .6;
}
.lang-toggle:hover,
.lang-toggle[aria-expanded="true"]{
background-color: transparent;
color: #666;
opacity: 1;
}
.lang-toggle-text{
display: inline-block;
min-width: 24px;
text-align: center;
font-size: 12px;
line-height: 24px;
font-weight: 700;
letter-spacing: .02em;
}
.lang-menu{
position: absolute;
top: calc(100% + .35rem);
right: 0;
min-width: 8rem;
padding: .25rem;
border: 1px solid rgb(229,231,235);
border-radius: .5rem;
background-color: rgb(255,255,255);
box-shadow: 0 8px 24px rgba(15,23,42,.12);
z-index: 10;
}
.lang-menu.hidden{
display: none;
}
.lang-menu-item{
width: 100%;
display: block;
text-align: left;
padding: .4rem .5rem;
border-radius: .35rem;
background: transparent;
color: #555;
font-size: .75rem;
line-height: 1.25rem;
padding: .15rem .35rem;
cursor: pointer;
}
.lang-menu-item:hover{
background-color: rgb(243,244,246);
}
.lang-menu-item.active{
background-color: rgb(220,252,231);
color: rgb(22,101,52);
font-weight: 600;
}
.tip{
margin-left: 36%;
+64 -11
View File
@@ -47,6 +47,36 @@ function formatSubstitutions(message, substitutions) {
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) {
@@ -100,7 +130,9 @@ function applyStaticI18n() {
setText('langOptionZhCN', 'langChineseSimplified')
setText('langOptionJa', 'langJapanese')
setText('langOptionKo', 'langKorean')
setTitle('langSelect', 'tipLanguage')
setTitle('langToggle', 'tipLanguage')
const langToggle = document.getElementById('langToggle')
if (langToggle) langToggle.setAttribute('aria-label', t('tipLanguage'))
// Native hover tooltips (title)
setTitle('opensite', 'tipOpenSite')
@@ -122,26 +154,47 @@ async function setUiLanguage(nextLang, { persist = true } = {}) {
currentUiLanguage = lang
overrideMessages = await loadLocaleMessages(lang)
applyStaticI18n()
const select = document.getElementById('langSelect')
if (select && select.value !== lang) select.value = lang
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 select = document.getElementById('langSelect')
if (select) {
select.addEventListener('change', async () => {
await setUiLanguage(select.value)
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)
})
}
const items = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
const stored = normalizeUiLanguage(items[UI_LANGUAGE_STORAGE_KEY])
if (select) select.value = stored
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
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2026.03.24",
"version": "2026.03.25",
"version_name": "Supports 0.15.0 - 0.26.x",
"action": {
"default_popup": "popup.html",
+16 -7
View File
@@ -13,13 +13,22 @@
<body class="body">
<div class="title" id="opensite">MEMOS</div>
<div id="lang_switcher" class="lang-switcher">
<select id="langSelect" class="lang-select" aria-label="Language" title="">
<option id="langOptionAuto" value="auto"></option>
<option id="langOptionEn" value="en"></option>
<option id="langOptionZhCN" value="zh_CN"></option>
<option id="langOptionJa" value="ja"></option>
<option id="langOptionKo" value="ko"></option>
</select>
<button
id="langToggle"
class="lang-toggle"
type="button"
aria-haspopup="true"
aria-expanded="false"
>
<span id="langToggleText" class="lang-toggle-text" aria-hidden="true">A</span>
</button>
<div id="langMenu" class="lang-menu hidden" role="menu" aria-labelledby="langToggle">
<button id="langOptionAuto" class="lang-menu-item" type="button" data-lang="auto" role="menuitemradio" aria-checked="false"></button>
<button id="langOptionEn" class="lang-menu-item" type="button" data-lang="en" role="menuitemradio" aria-checked="false"></button>
<button id="langOptionZhCN" class="lang-menu-item" type="button" data-lang="zh_CN" role="menuitemradio" aria-checked="false"></button>
<button id="langOptionJa" class="lang-menu-item" type="button" data-lang="ja" role="menuitemradio" aria-checked="false"></button>
<button id="langOptionKo" class="lang-menu-item" type="button" data-lang="ko" role="menuitemradio" aria-checked="false"></button>
</div>
</div>
<div id="blog_info_edit"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024">
<path d="M914 432c-5-26-21-43-41-43h-4c-54 0-99-44-99-99 0-17 9-37 9-38 10-22 2-50-18-65l-103-57h-1c-21-9-49-4-64 12-12 12-50 44-79 44s-68-33-79-45a60 60 0 0 0-64-13l-106 58-2 1a54 54 0 0 0-18 65c0 1 9 21 9 38 0 55-45 99-99 99h-5c-19 0-35 17-40 43 0 2-9 45-9 80s9 79 9 81c5 25 21 42 41 42h4c54 0 99 45 99 99 0 18-9 37-9 38-10 23-2 51 18 65l101 56 1 1c21 9 49 3 65-13 14-15 52-47 80-47 30 0 69 35 81 48a58 58 0 0 0 64 14l104-58 2-1c20-14 28-42 18-65 0-1-9-20-9-38 0-54 45-99 99-99h5c19 0 35-17 40-42 0-2 9-46 9-81s-9-78-9-80m-51 80c0 23-5 52-7 64a158 158 0 0 0-134 215l-89 49c-4-5-17-18-35-31-31-23-61-35-88-35s-57 12-88 34c-17 13-30 26-34 31l-86-48a159 159 0 0 0-134-215c-2-12-7-41-7-64 0-22 5-51 7-64a157 157 0 0 0 134-214l91-50c4 4 17 17 35 29 30 22 59 33 86 33s55-11 85-32c18-13 31-25 35-29l88 49a159 159 0 0 0 134 214c2 13 7 42 7 64"/>