Add UI language switcher and ja/ko locales

Introduce a runtime UI language switcher and add Japanese/Korean locale support. Added _locales/ja and _locales/ko messages, updated en and zh_CN message files with new language-related keys and sendImage label. Implemented dynamic i18n handling: new i18n.js supports override messages, persistent uiLanguage in storage, and emits i18n:changed; background.js now loads override locales, updates context menu titles and listens for storage changes. Integrated dayjs locales (js/ja.js, js/ko.js) and made oper.js use a unified msg() helper and react to language changes. Added language selector UI in popup.html and styling in css/main.css.
This commit is contained in:
jonny
2026-03-05 21:03:07 +08:00
parent c8bdb918f3
commit e57a963170
11 changed files with 752 additions and 113 deletions
+123 -43
View File
@@ -1,47 +1,127 @@
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create(
{
type: 'normal',
title: chrome.i18n.getMessage("sendTo"),
id: 'Memos-send-selection',
contexts: ['selection']
},
)
chrome.contextMenus.create(
{
type: 'normal',
title: chrome.i18n.getMessage("sendLinkTo"),
id: 'Memos-send-link',
contexts: ['link', 'page']
},
)
chrome.contextMenus.create(
{
type: 'normal',
title: chrome.i18n.getMessage("sendImageTo"),
id: 'Memos-send-image',
contexts: ['image']
},
)
})
chrome.contextMenus.onClicked.addListener(info => {
let tempCont=''
switch(info.menuItemId){
case 'Memos-send-selection':
tempCont = info.selectionText + '\n' + `[Reference Link](${info.linkUrl || info.pageUrl})` + '\n'
break
case 'Memos-send-link':
tempCont = (info.linkUrl || info.pageUrl) + '\n'
break
case 'Memos-send-image':
tempCont = `![](${info.srcUrl})` + '\n'
break
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 updateContextMenu(id, update) {
return new Promise((resolve) => {
try {
chrome.contextMenus.update(id, update, () => resolve())
} catch (_) {
resolve()
}
chrome.storage.sync.get({open_action: "save_text", open_content: ''}, function(items) {
if(items.open_action === 'upload_image') {
alert(chrome.i18n.getMessage("picPending"));
})
}
let cachedUiLanguage = null
let cachedOverrideMessages = null
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
}
}
async function getUiLanguage() {
const items = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
return normalizeUiLanguage(items[UI_LANGUAGE_STORAGE_KEY])
}
async function t(key) {
const lang = await getUiLanguage()
if (lang !== cachedUiLanguage) {
cachedUiLanguage = lang
cachedOverrideMessages = await loadLocaleMessages(lang)
}
const msg = cachedOverrideMessages && cachedOverrideMessages[key] && cachedOverrideMessages[key].message
if (typeof msg === 'string' && msg.length > 0) return msg
return chrome.i18n.getMessage(key) || ''
}
async function refreshContextMenus() {
await updateContextMenu('Memos-send-selection', { title: await t('sendTo') })
await updateContextMenu('Memos-send-link', { title: await t('sendLinkTo') })
await updateContextMenu('Memos-send-image', { title: await t('sendImageTo') })
}
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
type: 'normal',
title: chrome.i18n.getMessage('sendTo'),
id: 'Memos-send-selection',
contexts: ['selection']
})
chrome.contextMenus.create({
type: 'normal',
title: chrome.i18n.getMessage('sendLinkTo'),
id: 'Memos-send-link',
contexts: ['link', 'page']
})
chrome.contextMenus.create({
type: 'normal',
title: chrome.i18n.getMessage('sendImageTo'),
id: 'Memos-send-image',
contexts: ['image']
})
// Apply override titles if user selected a fixed language.
refreshContextMenus()
})
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'sync') return
if (!changes[UI_LANGUAGE_STORAGE_KEY]) return
cachedUiLanguage = null
cachedOverrideMessages = null
refreshContextMenus()
})
chrome.contextMenus.onClicked.addListener((info) => {
let tempCont = ''
switch (info.menuItemId) {
case 'Memos-send-selection':
tempCont =
info.selectionText +
'\n' +
`[Reference Link](${info.linkUrl || info.pageUrl})` +
'\n'
break
case 'Memos-send-link':
tempCont = (info.linkUrl || info.pageUrl) + '\n'
break
case 'Memos-send-image':
tempCont = `![](${info.srcUrl})` + '\n'
break
}
chrome.storage.sync.get(
{ open_action: 'save_text', open_content: '' },
function (items) {
if (items.open_action === 'upload_image') {
t('picPending').then((m) => alert(m))
} else {
chrome.storage.sync.set({open_action: "save_text", open_content: items.open_content + tempCont});
chrome.storage.sync.set({
open_action: 'save_text',
open_content: items.open_content + tempCont
})
}
})
}
)
})
+127 -29
View File
@@ -1,50 +1,148 @@
function getMessage(key) {
return chrome.i18n.getMessage(key) || ''
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 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 = getMessage(messageKey)
if (el) el.textContent = t(messageKey)
}
function setPlaceholder(id, messageKey) {
const el = document.getElementById(id)
if (el) el.placeholder = getMessage(messageKey)
if (el) el.placeholder = t(messageKey)
}
function setTitle(id, messageKey) {
const el = document.getElementById(id)
if (el) el.title = getMessage(messageKey)
if (el) el.title = t(messageKey)
}
setText("saveKey", "saveBtn")
setText("saveTag", "saveBtn")
function applyStaticI18n() {
setText('saveKey', 'saveBtn')
setText('saveTag', 'saveBtn')
setText("supportedMemosVersion", "supportedMemosVersion")
setText('supportedMemosVersion', 'supportedMemosVersion')
setPlaceholder("apiUrl", "placeApiUrl")
setPlaceholder("apiTokens", "placeApiTokens")
setPlaceholder("content", "placeContent")
setPlaceholder('apiUrl', 'placeApiUrl')
setPlaceholder('apiTokens', 'placeApiTokens')
setPlaceholder('content', 'placeContent')
setText("lockPrivate", "lockPrivate")
setText("lockProtected", "lockProtected")
setText("lockPublic", "lockPublic")
setText('lockPrivate', 'lockPrivate')
setText('lockProtected', 'lockProtected')
setText('lockPublic', 'lockPublic')
setText("content_submit_text", "submitBtn")
setText('content_submit_text', 'submitBtn')
setPlaceholder("hideInput", "placeHideInput")
setPlaceholder("showInput", "placeShowInput")
setPlaceholder('hideInput', 'placeHideInput')
setPlaceholder('showInput', 'placeShowInput')
setText("uploadlist-title", "uploadedListTitle")
setText('uploadlist-title', 'uploadedListTitle')
// 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")
// Language switcher
setText('langOptionAuto', 'langAuto')
setText('langOptionEn', 'langEnglish')
setText('langOptionZhCN', 'langChineseSimplified')
setText('langOptionJa', 'langJapanese')
setText('langOptionKo', 'langKorean')
setTitle('langSelect', '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')
}
async function setUiLanguage(nextLang, { persist = true } = {}) {
const lang = normalizeUiLanguage(nextLang)
currentUiLanguage = lang
overrideMessages = await loadLocaleMessages(lang)
applyStaticI18n()
const select = document.getElementById('langSelect')
if (select && select.value !== lang) select.value = 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 items = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
const stored = normalizeUiLanguage(items[UI_LANGUAGE_STORAGE_KEY])
if (select) select.value = stored
await setUiLanguage(stored, { persist: false })
}
window.t = t
window.setUiLanguage = setUiLanguage
window.getUiLanguage = () => currentUiLanguage
applyStaticI18n()
window.i18nReady = initLanguageSwitcher()
+1
View File
@@ -0,0 +1 @@
!function(e,_){"object"==typeof exports&&"undefined"!=typeof module?module.exports=_(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],_):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_locale_ja=_(e.dayjs)}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=_(e),a={name:"ja",relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1か月",MM:"%dか月",y:"1年",yy:"%d年"}};return t.default.locale(a,null,!0),a}));
+1
View File
@@ -0,0 +1 @@
!function(e,_){"object"==typeof exports&&"undefined"!=typeof module?module.exports=_(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],_):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_locale_ko=_(e.dayjs)}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=_(e),a={name:"ko",relativeTime:{future:"%s 후",past:"%s 전",s:"몇 초",m:"1분",mm:"%d분",h:"1시간",hh:"%d시간",d:"하루",dd:"%d일",M:"1개월",MM:"%d개월",y:"1년",yy:"%d년"}};return t.default.locale(a,null,!0),a}));
+99 -41
View File
@@ -1,5 +1,67 @@
dayjs.extend(window.dayjs_plugin_relativeTime)
dayjs.locale('zh-cn')
let currentMemoLock = ''
function msg(key) {
if (typeof window.t === 'function') return window.t(key)
return chrome.i18n.getMessage(key) || ''
}
function applyDayjsLocaleByUiLanguage(uiLang) {
const lang = String(uiLang || 'auto')
if (lang === 'zh_CN') {
dayjs.locale('zh-cn')
return
}
if (lang === 'ja') {
dayjs.locale('ja')
return
}
if (lang === 'ko') {
dayjs.locale('ko')
return
}
if (lang === 'en') {
dayjs.locale('en')
return
}
// auto: best-effort infer from browser UI language
const ui = String(chrome.i18n.getUILanguage ? chrome.i18n.getUILanguage() : '').toLowerCase()
if (ui.startsWith('zh')) {
dayjs.locale('zh-cn')
return
}
if (ui.startsWith('ja')) {
dayjs.locale('ja')
return
}
if (ui.startsWith('ko')) {
dayjs.locale('ko')
return
}
dayjs.locale('en')
}
function updateLockNowText(lockType) {
if (lockType === 'PUBLIC') {
$('#lock-now').text(msg('lockPublic'))
} else if (lockType === 'PRIVATE') {
$('#lock-now').text(msg('lockPrivate'))
} else if (lockType === 'PROTECTED') {
$('#lock-now').text(msg('lockProtected'))
}
}
applyDayjsLocaleByUiLanguage(typeof window.getUiLanguage === 'function' ? window.getUiLanguage() : 'auto')
window.addEventListener('i18n:changed', (ev) => {
applyDayjsLocaleByUiLanguage(ev && ev.detail ? ev.detail.lang : 'auto')
updateLockNowText(currentMemoLock)
renderUploadList(relistNow)
})
let relistNow = []
@@ -52,15 +114,10 @@ get_info(function (info) {
chrome.storage.sync.set(
{ memo_lock: 'PUBLIC' }
)
$("#lock-now").text(chrome.i18n.getMessage("lockPublic"))
}
if (memoNow == "PUBLIC") {
$("#lock-now").text(chrome.i18n.getMessage("lockPublic"))
} else if (memoNow == "PRIVATE") {
$("#lock-now").text(chrome.i18n.getMessage("lockPrivate"))
} else if (memoNow == "PROTECTED") {
$("#lock-now").text(chrome.i18n.getMessage("lockProtected"))
memoNow = 'PUBLIC'
}
currentMemoLock = memoNow
updateLockNowText(memoNow)
$('#apiUrl').val(info.apiUrl)
$('#apiTokens').val(info.apiTokens)
$('#hideInput').val(info.hidetag)
@@ -125,7 +182,7 @@ function initDrag() {
obj.ondragenter = function (ev) {
if (ev.target.className === 'common-editor-inputer') {
$.message({
message: chrome.i18n.getMessage("picDrag"),
message: msg('picDrag'),
autoClose: false
})
$('body').css('opacity', 0.3)
@@ -149,7 +206,7 @@ function initDrag() {
ev.preventDefault()
if (ev.target.className === 'common-editor-inputer') {
$.message({
message: chrome.i18n.getMessage("picCancelDrag")
message: msg('picCancelDrag')
})
$('body').css('opacity', 1)
}
@@ -179,8 +236,8 @@ function renderUploadList(list) {
if ($wrapper.length) $wrapper.show()
const tipReorder = escapeHtml(chrome.i18n.getMessage('tipReorder'))
const tipDelete = escapeHtml(chrome.i18n.getMessage('tipDeleteAttachment'))
const tipReorder = escapeHtml(msg('tipReorder'))
const tipDelete = escapeHtml(msg('tipDeleteAttachment'))
let html = ''
for (let i = 0; i < items.length; i++) {
@@ -274,7 +331,7 @@ $(document).on('click', '.upload-del', function () {
get_info(function (info) {
if (!info.status) {
$.message({ message: chrome.i18n.getMessage('placeApiUrl') })
$.message({ message: msg('placeApiUrl') })
return
}
@@ -287,19 +344,19 @@ $(document).on('click', '.upload-del', function () {
return x && x.name !== name
})
saveUploadList(next, function () {
$.message({ message: chrome.i18n.getMessage('attachmentDeleteSuccess') })
$.message({ message: msg('attachmentDeleteSuccess') })
renderUploadList(relistNow)
})
},
error: function () {
$.message({ message: chrome.i18n.getMessage('attachmentDeleteFailed') })
$.message({ message: msg('attachmentDeleteFailed') })
}
})
})
})
function uploadImage(file) {
$.message({
message: chrome.i18n.getMessage("picUploading"),
message: msg('picUploading'),
autoClose: false
});
const reader = new FileReader();
@@ -355,7 +412,7 @@ function uploadImageNow(base64String, file) {
resourceIdList: relistNow
},
function () {
$.message({ message: chrome.i18n.getMessage('picSuccess') })
$.message({ message: msg('picSuccess') })
}
)
} else {
@@ -366,18 +423,18 @@ function uploadImageNow(base64String, file) {
resourceIdList: []
},
function () {
$.message({ message: chrome.i18n.getMessage('picFailed') })
$.message({ message: msg('picFailed') })
}
)
}
},
function () {
$.message({ message: chrome.i18n.getMessage('picFailed') })
$.message({ message: msg('picFailed') })
}
)
}else {
$.message({
message: chrome.i18n.getMessage("placeApiUrl")
message: msg('placeApiUrl')
})
}
});
@@ -392,7 +449,7 @@ $('#saveKey').click(function () {
window.MemosApi.authWithFallback(apiUrl, apiTokens, function (auth) {
if (!auth || auth.userId == null) {
$.message({ message: chrome.i18n.getMessage('invalidToken') })
$.message({ message: msg('invalidToken') })
return
}
@@ -404,7 +461,7 @@ $('#saveKey').click(function () {
memoUiPath: auth.uiPath || 'memos'
},
function () {
$.message({ message: chrome.i18n.getMessage('saveSuccess') })
$.message({ message: msg('saveSuccess') })
$('#blog_info').hide()
}
)
@@ -446,12 +503,12 @@ $('#tags').click(function () {
$("#taglist").html(tagDom).slideToggle(500)
},
function () {
$.message({ message: chrome.i18n.getMessage('placeApiUrl') })
$.message({ message: msg('placeApiUrl') })
}
)
} else {
$.message({
message: chrome.i18n.getMessage("placeApiUrl")
message: msg('placeApiUrl')
})
}
})
@@ -470,7 +527,7 @@ $('#saveTag').click(function () {
},
function () {
$.message({
message: chrome.i18n.getMessage("saveSuccess")
message: msg('saveSuccess')
})
$('#taghide').hide()
}
@@ -485,6 +542,7 @@ $(document).on("click",".item-lock",function () {
$("#lock-wrapper").toggleClass( "!hidden", 1000 );
$("#lock-now").text($(this).text())
_this = $(this)[0].dataset.type;
currentMemoLock = _this
chrome.storage.sync.set(
{memo_lock: _this}
)
@@ -506,7 +564,7 @@ $('#search').click(function () {
let searchData = window.MemosApi.extractMemosListFromResponse(data)
if(searchData.length == 0){
$.message({
message: chrome.i18n.getMessage("searchNone")
message: msg('searchNone')
})
}else{
for(var i=0;i < searchData.length;i++){
@@ -540,17 +598,17 @@ $('#search').click(function () {
}
},
function () {
$.message({ message: chrome.i18n.getMessage('searchNone') })
$.message({ message: msg('searchNone') })
}
)
}else{
$.message({
message: chrome.i18n.getMessage("searchNow")
message: msg('searchNow')
})
}
} else {
$.message({
message: chrome.i18n.getMessage("placeApiUrl")
message: msg('placeApiUrl')
})
}
})
@@ -572,12 +630,12 @@ $('#random').click(function () {
randDom(randomData)
},
function () {
$.message({ message: chrome.i18n.getMessage('placeApiUrl') })
$.message({ message: msg('placeApiUrl') })
}
)
} else {
$.message({
message: chrome.i18n.getMessage("placeApiUrl")
message: msg('placeApiUrl')
})
}
})
@@ -639,11 +697,11 @@ get_info(function (info) {
success: function(result){
$("#randomlist").html('').hide()
$.message({
message: chrome.i18n.getMessage("archiveSuccess")
message: msg('archiveSuccess')
})
},error:function(err){//清空open_action(打开时候进行的操作),同时清空open_content
$.message({
message: chrome.i18n.getMessage("archiveFailed")
message: msg('archiveFailed')
})
}
})
@@ -667,7 +725,7 @@ $('#getlink').click(function () {
add(linkHtml);
}else{
$.message({
message: chrome.i18n.getMessage("getTabFailed")
message: msg('getTabFailed')
})
}
})
@@ -711,7 +769,7 @@ $('#content_submit_text').click(function () {
sendText()
}else{
$.message({
message: chrome.i18n.getMessage("placeContent")
message: msg('placeContent')
})
}
})
@@ -733,7 +791,7 @@ function getOne(memosId){
})
} else {
$.message({
message: chrome.i18n.getMessage("placeApiUrl")
message: msg('placeApiUrl')
})
}
})
@@ -743,7 +801,7 @@ function sendText() {
get_info(function (info) {
if (info.status) {
$.message({
message: chrome.i18n.getMessage("memoUploading")
message: msg('memoUploading')
})
//$("#content_submit_text").attr('disabled','disabled');
let content = $("textarea[name=text]").val()
@@ -789,7 +847,7 @@ function sendText() {
{ open_action: '', open_content: '',resourceIdList:[]},
function () {
$.message({
message: chrome.i18n.getMessage("memoSuccess")
message: msg('memoSuccess')
})
//$("#content_submit_text").removeAttr('disabled');
$("textarea[name=text]").val('')
@@ -802,14 +860,14 @@ function sendText() {
{ open_action: '', open_content: '',resourceIdList:[] },
function () {
$.message({
message: chrome.i18n.getMessage("memoFailed")
message: msg('memoFailed')
})
}
)},
})
} else {
$.message({
message: chrome.i18n.getMessage("placeApiUrl")
message: msg('placeApiUrl')
})
}
})