feat(i18n): add English localization with per-user language preference and navbar toggle

This commit is contained in:
2026-07-06 22:07:05 +08:00
parent 172e33e121
commit f448a900f5
17 changed files with 539 additions and 418 deletions

212
app/i18n.py Normal file
View File

@@ -0,0 +1,212 @@
# Translation keys for zh (Chinese) and en (English)
_translations: dict[str, dict[str, str]] = {
# ── Brand / Nav ──
"brand": {"zh": "📮 星笺", "en": "📮 Mailova"},
"nav.profiles": {"zh": "Profile", "en": "Profiles"},
"nav.settings": {"zh": "设置", "en": "Settings"},
"nav.invites": {"zh": "邀请码管理", "en": "Invites"},
"nav.logout": {"zh": "退出", "en": "Logout"},
# ── Auth ──
"login.title": {"zh": "登录", "en": "Login"},
"login.username": {"zh": "用户名", "en": "Username"},
"login.password": {"zh": "密码", "en": "Password"},
"login.remember": {"zh": "记住我", "en": "Remember me"},
"login.no_account": {"zh": "没有账号?", "en": "No account?"},
"login.go_register": {"zh": "去注册", "en": "Sign up"},
"register.title": {"zh": "注册", "en": "Register"},
"register.hint": {"zh": "邀请码由已有用户或管理员提供", "en": "Invite code provided by an existing user or admin"},
"register.invite_code": {"zh": "邀请码", "en": "Invite code"},
"register.invite_ph": {"zh": "8位字母数字", "en": "8-char code"},
"register.username": {"zh": "用户名", "en": "Username"},
"register.password": {"zh": "密码", "en": "Password"},
"register.confirm_password": {"zh": "确认密码", "en": "Confirm password"},
"register.has_account": {"zh": "已有账号?", "en": "Already have an account?"},
"register.go_login": {"zh": "去登录", "en": "Log in"},
# ── Dashboard ──
"dash.welcome": {"zh": "👋 欢迎,", "en": "👋 Welcome, "},
"dash.total_cards": {"zh": "全部明信片", "en": "Total postcards"},
"dash.sent": {"zh": "已寄出", "en": "Sent"},
"dash.delivered": {"zh": "已送达", "en": "Delivered"},
"dash.received": {"zh": "已收到", "en": "Received"},
"dash.countries": {"zh": "国家/地区", "en": "Countries"},
"dash.chart_sent": {"zh": "📤 寄出", "en": "📤 Sent"},
"dash.chart_received": {"zh": "📬 收到", "en": "📬 Received"},
# ── Profiles ──
"profiles.title": {"zh": "Profile", "en": "Profiles"},
"profiles.new_ph": {"zh": "新 Profile 昵称", "en": "New profile nickname"},
"profiles.new_btn": {"zh": "+ 新建", "en": "+ New"},
"profiles.rename": {"zh": "改名", "en": "Rename"},
"profiles.total": {"zh": "总计", "en": "Total"},
"profiles.countries": {"zh": "国家", "en": "Countries"},
"profiles.notes_ph": {"zh": "备注(仅自己可见)", "en": "Notes (private)"},
"profiles.save": {"zh": "保存", "en": "Save"},
"profiles.created_at": {"zh": "创建于", "en": "Created"},
"profiles.view": {"zh": "查看", "en": "View"},
"profiles.delete": {"zh": "删除", "en": "Delete"},
"profiles.confirm_delete": {"zh": "确定删除此 Profile", "en": "Delete this profile?"},
# ── Postcards list ──
"pc_list.title": {"zh": " 的明信片", "en": "'s postcards"},
"pc_list.new": {"zh": "+ 新明信片", "en": "+ New postcard"},
"pc_list.all_countries": {"zh": "全部国家", "en": "All countries"},
"pc_list.filter": {"zh": "筛选", "en": "Filter"},
"pc_list.tab_sent": {"zh": "寄出", "en": "Sent"},
"pc_list.tab_received": {"zh": "收到", "en": "Received"},
"pc_list.mark_delivered": {"zh": "已送达", "en": "Delivered"},
"pc_list.confirm_delivered": {"zh": "确认已送达?", "en": "Mark as delivered?"},
"pc_list.empty_sent": {"zh": "没有寄出的明信片。", "en": "No sent postcards."},
"pc_list.empty_received": {"zh": "没有收到的明信片。", "en": "No received postcards."},
# ── Postcard detail ──
"pc_detail.title": {"zh": "明信片详情", "en": "Postcard detail"},
"pc_detail.edit": {"zh": "编辑", "en": "Edit"},
"pc_detail.mark_delivered": {"zh": "已送达", "en": "Mark delivered"},
"pc_detail.confirm_delivered": {"zh": "确认已送达?", "en": "Mark as delivered?"},
"pc_detail.delete": {"zh": "删除", "en": "Delete"},
"pc_detail.confirm_delete": {"zh": "确定删除?", "en": "Delete this postcard?"},
"pc_detail.front": {"zh": "正面", "en": "Front"},
"pc_detail.back": {"zh": "反面", "en": "Back"},
"pc_detail.no_front": {"zh": "📷 未上传正面图片", "en": "📷 No front image"},
"pc_detail.no_back": {"zh": "📷 未上传反面图片", "en": "📷 No back image"},
"pc_detail.upload_front": {"zh": "上传正面", "en": "Upload front"},
"pc_detail.upload_back": {"zh": "上传反面", "en": "Upload back"},
"pc_detail.card_number": {"zh": "编号", "en": "Number"},
"pc_detail.status": {"zh": "状态", "en": "Status"},
"pc_detail.recipient": {"zh": "收件人", "en": "Recipient"},
"pc_detail.country": {"zh": "国家/地区", "en": "Country"},
"pc_detail.send_time": {"zh": "寄出时间", "en": "Sent date"},
"pc_detail.arrival_time": {"zh": "到达时间", "en": "Arrival date"},
"pc_detail.sender": {"zh": "发件人", "en": "Sender"},
"pc_detail.receive_time": {"zh": "收到时间", "en": "Received date"},
"pc_detail.profile": {"zh": "所属 Profile", "en": "Profile"},
"pc_detail.notes": {"zh": "备注", "en": "Notes"},
# ── Status labels ──
"status.sent": {"zh": "已寄出", "en": "Sent"},
"status.delivered": {"zh": "已送达", "en": "Delivered"},
"status.received": {"zh": "已收到", "en": "Received"},
# ── Postcard form ──
"pc_form.edit": {"zh": "编辑明信片", "en": "Edit postcard"},
"pc_form.new": {"zh": "新建明信片", "en": "New postcard"},
"pc_form.number": {"zh": "编号", "en": "Number"},
"pc_form.number_hint": {"zh": "格式AB-1234567", "en": "Format: AB-1234567"},
"pc_form.status": {"zh": "状态", "en": "Status"},
"pc_form.recipient": {"zh": "收件人", "en": "Recipient"},
"pc_form.country": {"zh": "国家/地区", "en": "Country"},
"pc_form.send_time": {"zh": "寄出时间", "en": "Sent date"},
"pc_form.arrival_time": {"zh": "到达时间", "en": "Arrival date"},
"pc_form.sender": {"zh": "发件人", "en": "Sender"},
"pc_form.receive_time": {"zh": "收到时间", "en": "Received date"},
"pc_form.notes": {"zh": "备注", "en": "Notes"},
"pc_form.notes_ph": {"zh": "可选备注信息", "en": "Optional notes"},
"pc_form.save": {"zh": "保存", "en": "Save"},
"pc_form.cancel": {"zh": "取消", "en": "Cancel"},
# ── Settings ──
"settings.title": {"zh": "用户设置", "en": "Settings"},
"settings.tab_account": {"zh": "账号", "en": "Account"},
"settings.tab_showcase": {"zh": "展示", "en": "Showcase"},
"settings.change_username": {"zh": "修改用户名", "en": "Change username"},
"settings.current_username": {"zh": "当前用户名", "en": "Current username"},
"settings.new_username": {"zh": "新用户名", "en": "New username"},
"settings.change_password": {"zh": "修改密码", "en": "Change password"},
"settings.current_password": {"zh": "当前密码", "en": "Current password"},
"settings.new_password": {"zh": "新密码", "en": "New password"},
"settings.confirm_password": {"zh": "确认新密码", "en": "Confirm new password"},
"settings.language": {"zh": "语言", "en": "Language"},
"settings.language_zh": {"zh": "中文", "en": "中文"},
"settings.language_en": {"zh": "English", "en": "English"},
"settings.showcase_global": {"zh": "全局展示设置", "en": "Global showcase settings"},
"settings.showcase_mode": {"zh": "展示页面模式", "en": "Showcase mode"},
"settings.mode_profile": {"zh": "按 Profile 分类", "en": "Grouped by profile"},
"settings.mode_flat": {"zh": "全部合并", "en": "All together"},
"settings.save_global": {"zh": "保存全局设置", "en": "Save global settings"},
"settings.public_url": {"zh": "公开展示页地址:", "en": "Public showcase: "},
"settings.public_url_label": {"zh": "公开展示页:", "en": "Public showcase: "},
"settings.profiles_showcase": {"zh": "各 Profile 展示设置", "en": "Per-profile showcase"},
"settings.showcase_on": {"zh": "展示中", "en": "Showing"},
"settings.showcase_off": {"zh": "未开启", "en": "Off"},
"settings.enable_showcase": {"zh": "开启展示", "en": "Enable showcase"},
"settings.showcase_content": {"zh": "展示内容", "en": "Content"},
"settings.content_both": {"zh": "发出 + 收到", "en": "Sent + Received"},
"settings.content_sent": {"zh": "仅发出", "en": "Sent only"},
"settings.content_received": {"zh": "仅收到", "en": "Received only"},
"settings.save": {"zh": "保存", "en": "Save"},
# ── Admin invites ──
"invites.title": {"zh": "邀请码管理", "en": "Invite codes"},
"invites.create": {"zh": "创建邀请码", "en": "Create invite code"},
"invites.code_label": {"zh": "邀请码(留空自动生成)", "en": "Code (leave empty to auto-generate)"},
"invites.code_ph": {"zh": "自动生成", "en": "Auto-generate"},
"invites.limit_type": {"zh": "限制类型", "en": "Limit type"},
"invites.unlimited": {"zh": "无限制", "en": "Unlimited"},
"invites.uses": {"zh": "使用次数", "en": "Max uses"},
"invites.expires": {"zh": "有效期", "en": "Expiration"},
"invites.times": {"zh": "次数", "en": "Times"},
"invites.create_btn": {"zh": "创建", "en": "Create"},
"invites.creator": {"zh": "创建者", "en": "By"},
"invites.used": {"zh": "已用", "en": "Used"},
"invites.no_expire": {"zh": "永不过期", "en": "No expiration"},
"invites.expires_at": {"zh": "过期", "en": "Expires"},
"invites.edit": {"zh": "编辑", "en": "Edit"},
"invites.save": {"zh": "保存", "en": "Save"},
"invites.delete": {"zh": "删除", "en": "Delete"},
"invites.confirm_delete": {"zh": "确定删除此邀请码?", "en": "Delete this invite code?"},
"invites.max_uses": {"zh": "最大使用次数", "en": "Max uses"},
"invites.expires_time": {"zh": "过期时间", "en": "Expiration"},
"invites.registered_users": {"zh": "注册用户", "en": "Registered users"},
# ── Showcase (public) ──
"showcase.title": {"zh": " 的明信片", "en": "'s postcards"},
"showcase.empty": {"zh": "暂无展示内容", "en": "Nothing to show yet"},
"showcase.tab_sent": {"zh": "发出", "en": "Sent"},
"showcase.tab_received": {"zh": "收到", "en": "Received"},
# ── Showcase manage ──
"showcase_manage.title": {"zh": "展示管理", "en": "Showcase manager"},
"showcase_manage.global": {"zh": "全局展示设置", "en": "Global showcase settings"},
"showcase_manage.save": {"zh": "保存全局设置", "en": "Save global settings"},
"showcase_manage.public_url": {"zh": "公开展示页地址:", "en": "Public showcase: "},
"showcase_manage.profiles": {"zh": "各 Profile 展示设置", "en": "Per-profile settings"},
"showcase_manage.on": {"zh": "展示中", "en": "Showing"},
"showcase_manage.off": {"zh": "未开启", "en": "Off"},
"showcase_manage.enable": {"zh": "开启展示", "en": "Enable"},
"showcase_manage.content": {"zh": "展示内容", "en": "Content"},
"showcase_manage.both": {"zh": "发出 + 收到", "en": "Sent + Received"},
"showcase_manage.sent_only": {"zh": "仅发出", "en": "Sent only"},
"showcase_manage.received_only": {"zh": "仅收到", "en": "Received only"},
# ── Profile showcase ──
"profile_showcase.title": {"zh": " 展示设置", "en": " showcase settings"},
"profile_showcase.enable": {"zh": "开启展示", "en": "Enable showcase"},
"profile_showcase.content": {"zh": "展示内容", "en": "Content"},
"profile_showcase.both": {"zh": "发出 + 收到", "en": "Sent + Received"},
"profile_showcase.sent_only": {"zh": "仅发出", "en": "Sent only"},
"profile_showcase.received_only": {"zh": "仅收到", "en": "Received only"},
"profile_showcase.single_control": {"zh": "单张明信片展示控制", "en": "Per-card visibility"},
"profile_showcase.hidden": {"zh": "隐藏", "en": "Hidden"},
"profile_showcase.hint": {"zh": "开启展示后可控制单张明信片的可见性", "en": "Enable showcase to control per-card visibility"},
# ── Flash messages ──
"flash.username_taken": {"zh": "用户名已存在", "en": "Username already taken"},
"flash.username_ok": {"zh": "用户名修改成功", "en": "Username changed"},
"flash.wrong_password": {"zh": "当前密码错误", "en": "Wrong password"},
"flash.password_mismatch": {"zh": "两次输入的新密码不一致", "en": "Passwords don't match"},
"flash.password_short": {"zh": "新密码长度至少6位", "en": "Password must be at least 6 characters"},
"flash.password_ok": {"zh": "密码修改成功", "en": "Password changed"},
# ── Misc ──
"misc.required": {"zh": " *", "en": " *"},
}
def t(key: str, lang: str = "zh") -> str:
"""Translate a key to the given language. Falls back to zh, then key itself."""
entry = _translations.get(key)
if entry is None:
return key
return entry.get(lang) or entry.get("zh") or key