Files
yoresee_dropbox/web/components/i18n.js
T
XingfenD 5bb42a8b8a feat(web): add i18n, light/dark theme switching, and version constant
- Add i18n (English + 简体中文) with a locale store, $t() magic, and a language toggle (persisted)
- Add light/dark/system theme switching via a theme store and [data-theme] token overrides
- Set the theme before paint to avoid a flash of the wrong theme
- Add frontend version constant web/version.js (window.YDROPBX_VERSION) shown in the app footer
- Update AGENTS.md version-control convention to mention web/version.js
- Bump version to 0.1.1 across frontend, webui changelog, and backend constant
2026-08-27 15:21:21 +08:00

121 lines
3.8 KiB
JavaScript

/**
* Internationalization (i18n).
*
* - `window.I18n` holds the message catalogs (currently English + 简体中文).
* - The `locale` store owns the active language, persists it to localStorage,
* and exposes `t(key)`.
* - `$t('key')` is registered as an Alpine magic so templates can translate
* inline (e.g. `x-text="$t('nav.logout')"`).
*
* Because `t()` reads the reactive `lang`, any string bound through it updates
* automatically when the language changes.
*/
window.I18n = {
en: {
'app.name': 'yDropbox',
'nav.logout': 'Logout',
'inbox': 'Inbox',
'outbox': 'Outbox',
'dropzone.click': 'Click to upload',
'dropzone.rest': 'or drag and drop',
'empty.inbox': 'No files in inbox',
'empty.outbox': 'No files in outbox',
'action.download': 'Download',
'action.share': 'Share',
'action.delete': 'Delete',
'toast.uploaded': 'File uploaded',
'toast.deleted': 'File deleted',
'toast.loadFailed': 'Failed to load files',
'toast.shareCopied': 'Share link copied to clipboard',
'confirm.deleteTitle': 'Delete file',
'confirm.deleteMessage': 'This file will be permanently removed. Continue?',
'confirm.cancel': 'Cancel',
'confirm.delete': 'Delete',
'confirm.confirm': 'Confirm',
'theme.light': 'Light',
'theme.dark': 'Dark',
'theme.system': 'System',
'lang.label': 'Language',
'login.subtitle': 'Enter your access token to continue',
'login.token': 'Access Token',
'login.submit': 'Sign In',
'login.error': 'Invalid token. Please try again.',
},
zh: {
'app.name': 'yDropbox',
'nav.logout': '退出登录',
'inbox': '收件箱',
'outbox': '发件箱',
'dropzone.click': '点击上传',
'dropzone.rest': '或拖拽文件到此处',
'empty.inbox': '收件箱暂无文件',
'empty.outbox': '发件箱暂无文件',
'action.download': '下载',
'action.share': '分享',
'action.delete': '删除',
'toast.uploaded': '文件已上传',
'toast.deleted': '文件已删除',
'toast.loadFailed': '文件加载失败',
'toast.shareCopied': '分享链接已复制到剪贴板',
'confirm.deleteTitle': '删除文件',
'confirm.deleteMessage': '该文件将被永久删除,是否继续?',
'confirm.cancel': '取消',
'confirm.delete': '删除',
'confirm.confirm': '确认',
'theme.light': '浅色',
'theme.dark': '深色',
'theme.system': '跟随系统',
'lang.label': '语言',
'login.subtitle': '请输入访问令牌以继续',
'login.token': '访问令牌',
'login.submit': '登录',
'login.error': '令牌无效,请重试。',
},
};
(function () {
function register() {
Alpine.store('locale', {
lang: 'en',
init() {
let saved = null;
try {
saved = localStorage.getItem('yd.locale');
} catch (e) {}
const browser = (navigator.language || 'en').toLowerCase().startsWith('zh') ? 'zh' : 'en';
this.lang = saved || browser;
this._applyLangAttr();
},
set(lang) {
this.lang = lang;
try {
localStorage.setItem('yd.locale', lang);
} catch (e) {}
this._applyLangAttr();
},
toggle() {
this.set(this.lang === 'en' ? 'zh' : 'en');
},
t(key) {
const dict = window.I18n[this.lang] || window.I18n.en;
if (dict && dict[key] != null) return dict[key];
if (window.I18n.en[key] != null) return window.I18n.en[key];
return key;
},
_applyLangAttr() {
document.documentElement.setAttribute('lang', this.lang === 'zh' ? 'zh-CN' : 'en');
},
});
Alpine.magic('t', () => (key) => Alpine.store('locale').t(key));
}
if (window.Alpine) register();
else document.addEventListener('alpine:init', register);
})();