Files
go_blog/i18n/i18n.go
T
kevinandClaude Fable 5 e872c08f3b feat: article attachments with content-addressed dedup
Add an attachments table and AJAX upload/management on the article
create and edit pages.

- Attachment model with article_id (0 while pending on create),
  session_token ownership, and SHA-256 content-addressed stored_name
- Upload validates via the platform policy (switch/type/size) and
  deduplicates on disk by content hash
- Plan-A binding: attachments uploaded before an article exists are
  owned by a session token and bound to the new article on save
- Delete soft-removes the record and drops the disk file only when no
  remaining rows reference it (reference counting for deduped files)
- Edit page loads existing attachments via JSON list endpoint
- Row actions: insert into body (markdown image/link), set as cover
  (image only), and delete
- Download URLs use the configured default download base URL when set,
  else the local /uploads path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-21 21:02:09 +08:00

433 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package i18n
import (
"strings"
)
// Lang represents a supported language code.
type Lang string
const (
EN Lang = "en"
ZH Lang = "zh"
)
// translations holds all UI strings keyed by language then translation key.
var translations = map[Lang]map[string]string{
EN: {
// Nav
"site_title": "Go Blog",
"home": "Home",
"login": "Login",
"dashboard": "Dashboard",
"logout": "Logout",
// Home page
"home_welcome": "Welcome to Go Blog",
"home_subtitle": "A simple, fast blog engine built with Go, Gin, and Tailwind CSS.",
"home_sign_in": "Sign In",
"home_to_dash": "Dashboard",
"home_latest": "Latest Posts",
"home_read_more": "Read more",
"home_post1_tit": "Getting Started",
"home_post1_dsc": "Welcome to your new blog! This is a placeholder post. Start writing to see your content here.",
"home_post2_tit": "Hello World",
"home_post2_dsc": "Blog posts will appear here once you start publishing from the admin dashboard.",
"home_post3_tit": "Built with Go",
"home_post3_dsc": "This blog engine uses Go, Gin web framework, GORM, and Tailwind CSS for a modern experience.",
"home_no_posts": "No published posts yet.",
// Login page
"login_title": "Sign In",
"login_username": "Username",
"login_password": "Password",
"login_ph_user": "Enter your username",
"login_ph_pass": "Enter your password",
"login_submit": "Sign In",
"login_error": "Invalid username or password.",
"login_required": "Please fill in all fields.",
// Dashboard
"dash_title": "Dashboard",
"dash_welcome": "Welcome back,",
"dash_posts": "Posts",
"dash_pages": "Pages",
"dash_users": "Users",
"dash_published": "Published",
"dash_mgmt_title": "Blog Management",
"dash_mgmt_desc": "Post management, categories, and settings will appear here in future updates.",
"dash_new_article": "New Article",
"dash_page_title": "Dashboard",
// Footer
"footer_text": "© 2026 Go Blog. Powered by Go & Gin.",
// Page titles
"page_home": "Home",
"page_login": "Login",
// Language switcher
"lang_switch": "中文",
// Profile dropdown & page
"profile": "Profile",
"admin_panel": "Admin Panel",
"profile_title": "Edit Profile",
"profile_avatar": "Avatar",
"profile_change_avatar": "Change Avatar",
"profile_display_name": "Display Name",
"profile_gender": "Gender",
"profile_birthday": "Birthday",
"profile_email": "Email",
"profile_change_password": "Change Password",
"profile_current_password": "Current Password",
"profile_new_password": "New Password",
"profile_save": "Save Changes",
"profile_saved": "Profile updated.",
"profile_wrong_password": "Current password is incorrect.",
"profile_upload_invalid": "File type not allowed.",
"profile_upload_disabled": "Uploads are currently disabled.",
"profile_upload_too_large": "File is too large. Limit: %s",
"gender_male": "Male",
"gender_female": "Female",
"gender_other": "Other",
// Avatar cropping
"crop_avatar_title": "Crop Avatar",
"crop_cancel": "Cancel",
"crop_confirm": "Confirm",
"crop_uploading": "Uploading...",
"crop_success": "Avatar updated.",
"crop_error": "Failed to upload avatar. Please try again.",
// Article creation
"article_create_title": "Create Article",
"article_title": "Title",
"article_slug": "Slug",
"article_slug_hint": "Auto-generated from title if empty",
"article_summary": "Summary",
"article_content": "Content",
"article_cover": "Cover Image URL",
"article_status": "Status",
"article_draft": "Draft",
"article_published": "Published",
"article_is_top": "Pin to top",
"article_save_draft": "Save Draft",
"article_publish": "Publish",
"article_created": "Article created successfully.",
"article_error": "Failed to create article.",
"article_new": "New Article",
"article_title_required": "Title is required.",
"article_content_required": "Content is required.",
"article_back_home": "Back to Home",
"article_not_found": "Article not found.",
"article_attachments": "Attachments",
"article_upload": "Upload",
"article_att_name": "File",
"article_att_size": "Size",
"article_att_insert": "Insert",
"article_att_set_cover": "Set as Cover",
"article_att_cover_set": "Cover URL set.",
"article_att_pick": "Please choose a file.",
"article_att_uploading": "Uploading...",
"article_att_error": "Upload failed. Please try again.",
"article_att_delete_confirm": "Delete this attachment?",
// Article management
"article_list_title": "Articles",
"article_edit_title": "Edit Article",
"article_manage": "Manage Articles",
"article_edit": "Edit",
"article_delete": "Delete",
"article_delete_confirm": "Delete this article?",
"article_updated": "Article updated.",
"article_deleted": "Article deleted.",
"article_archived": "Archived",
"article_col_title": "Title",
"article_col_status": "Status",
"article_col_published": "Published",
"article_col_actions": "Actions",
// Settings (platform configuration)
"settings_nav": "Platform Settings",
"settings_saved": "Settings saved.",
"settings_site_title": "Site Settings",
"settings_site_desc": "Logo, top-left title, header banner, and footer text.",
"settings_logo": "Logo",
"settings_logo_url": "Logo URL (external link)",
"settings_logo_upload": "Or upload a logo image",
"settings_logo_clear": "Remove current logo",
"settings_logo_text_zh": "Site Title (Chinese)",
"settings_logo_text_en": "Site Title (English)",
"settings_header_text_zh": "Header Banner Text (Chinese)",
"settings_header_text_en": "Header Banner Text (English)",
"settings_home_welcome_zh": "Home Welcome Heading (Chinese)",
"settings_home_welcome_en": "Home Welcome Heading (English)",
"settings_home_subtitle_zh": "Home Subtitle (Chinese)",
"settings_home_subtitle_en": "Home Subtitle (English)",
"settings_footer_text_zh": "Footer Text (Chinese)",
"settings_footer_text_en": "Footer Text (English)",
"settings_leave_blank": "Leave blank to use the built-in default.",
"settings_save": "Save",
"settings_upload_title": "Upload Settings",
"settings_upload_desc": "Attachment upload policy and permitted file types.",
"settings_uploads_enabled":"Enable attachments",
"settings_default_size": "Default max size (MB)",
"settings_storage_dir": "Storage sub-directory",
"settings_file_types": "Permitted File Types",
"settings_add_type": "Add File Type",
"settings_extension": "Extension",
"settings_mime": "MIME Type",
"settings_category": "Category",
"settings_max_size": "Max Size (MB)",
"settings_enabled": "Enabled",
"settings_actions": "Actions",
"settings_toggle": "Toggle",
"settings_delete": "Delete",
"settings_update": "Update",
"settings_download_title": "Download Settings",
"settings_download_desc": "Base URLs used to build attachment download links.",
"settings_add_url": "Add Base URL",
"settings_url_name": "Name",
"settings_base_url": "Base URL",
"settings_priority": "Priority",
"settings_is_default": "Default",
"settings_set_default": "Set Default",
"cat_image": "Image",
"cat_document": "Document",
"cat_archive": "Archive",
"cat_video": "Video",
"cat_other": "Other",
},
ZH: {
// 导航
"site_title": "Go 博客",
"home": "首页",
"login": "登录",
"dashboard": "后台",
"logout": "退出",
// 首页
"home_welcome": "欢迎来到 Go Blog",
"home_subtitle": "一个使用 Go、Gin 和 Tailwind CSS 构建的简洁快速的博客引擎。",
"home_sign_in": "登录",
"home_to_dash": "后台管理",
"home_latest": "最新文章",
"home_read_more": "阅读全文",
"home_post1_tit": "入门指南",
"home_post1_dsc": "欢迎来到你的新博客!这是一篇占位文章。开始写作后,你的内容将显示在这里。",
"home_post2_tit": "你好,世界",
"home_post2_dsc": "当你从后台管理面板发布文章后,博客文章将显示在这里。",
"home_post3_tit": "使用 Go 构建",
"home_post3_dsc": "此博客引擎使用 Go、Gin Web 框架、GORM 和 Tailwind CSS 构建,提供现代化的体验。",
"home_no_posts": "暂无已发布的文章。",
// 登录页
"login_title": "登录",
"login_username": "用户名",
"login_password": "密码",
"login_ph_user": "请输入用户名",
"login_ph_pass": "请输入密码",
"login_submit": "登录",
"login_error": "用户名或密码错误。",
"login_required": "请填写所有字段。",
// 后台
"dash_title": "后台管理",
"dash_welcome": "欢迎回来,",
"dash_posts": "文章",
"dash_pages": "页面",
"dash_users": "用户",
"dash_published": "已发布",
"dash_mgmt_title": "博客管理",
"dash_mgmt_desc": "文章管理、分类和设置将在后续版本中添加。",
"dash_new_article": "新建文章",
"dash_page_title": "后台管理",
// 页脚
"footer_text": "© 2026 Go Blog。由 Go & Gin 驱动。",
"page_home": "首页",
"page_login": "登录",
// 语言切换
"lang_switch": "English",
// 个人中心下拉菜单 & 页面
"profile": "个人信息",
"admin_panel": "后台管理",
"profile_title": "编辑个人信息",
"profile_avatar": "头像",
"profile_change_avatar": "更换头像",
"profile_display_name": "显示名称",
"profile_gender": "性别",
"profile_birthday": "生日",
"profile_email": "邮箱",
"profile_change_password": "修改密码",
"profile_current_password": "当前密码",
"profile_new_password": "新密码",
"profile_save": "保存修改",
"profile_saved": "个人信息已更新。",
"profile_wrong_password": "当前密码错误。",
"profile_upload_invalid": "不允许的文件类型。",
"profile_upload_disabled": "上传功能已关闭。",
"profile_upload_too_large": "文件过大。限制:%s",
"gender_male": "男",
"gender_female": "女",
"gender_other": "其他",
// 头像裁剪
"crop_avatar_title": "裁剪头像",
"crop_cancel": "取消",
"crop_confirm": "确认",
"crop_uploading": "上传中...",
"crop_success": "头像已更新。",
"crop_error": "头像上传失败,请重试。",
// 文章创建
"article_create_title": "新建文章",
"article_title": "标题",
"article_slug": "Slug",
"article_slug_hint": "留空则从标题自动生成",
"article_summary": "摘要",
"article_content": "正文",
"article_cover": "封面图 URL",
"article_status": "状态",
"article_draft": "草稿",
"article_published": "已发布",
"article_is_top": "置顶",
"article_save_draft": "保存草稿",
"article_publish": "发布",
"article_created": "文章创建成功。",
"article_error": "创建文章失败。",
"article_new": "新建文章",
"article_title_required": "标题不能为空。",
"article_content_required": "正文不能为空。",
"article_back_home": "返回首页",
"article_not_found": "文章不存在。",
"article_attachments": "附件",
"article_upload": "上传",
"article_att_name": "文件",
"article_att_size": "大小",
"article_att_insert": "插入正文",
"article_att_set_cover": "设为封面",
"article_att_cover_set": "已设为封面。",
"article_att_pick": "请先选择文件。",
"article_att_uploading": "上传中...",
"article_att_error": "上传失败,请重试。",
"article_att_delete_confirm": "删除该附件?",
// 文章管理
"article_list_title": "文章管理",
"article_edit_title": "编辑文章",
"article_manage": "文章管理",
"article_edit": "编辑",
"article_delete": "删除",
"article_delete_confirm": "确认删除这篇文章?",
"article_updated": "文章已更新。",
"article_deleted": "文章已删除。",
"article_archived": "已归档",
"article_col_title": "标题",
"article_col_status": "状态",
"article_col_published": "发布时间",
"article_col_actions": "操作",
// 平台设置
"settings_nav": "平台设置",
"settings_saved": "设置已保存。",
"settings_site_title": "站点设置",
"settings_site_desc": "Logo、左上角标题、顶部横幅文本和页脚文本。",
"settings_logo": "Logo",
"settings_logo_url": "Logo 链接(外链地址)",
"settings_logo_upload": "或上传 Logo 图片",
"settings_logo_clear": "移除当前 Logo",
"settings_logo_text_zh": "站点标题(中文)",
"settings_logo_text_en": "站点标题(英文)",
"settings_header_text_zh": "顶部横幅文本(中文)",
"settings_header_text_en": "顶部横幅文本(英文)",
"settings_home_welcome_zh": "主页欢迎标题(中文)",
"settings_home_welcome_en": "主页欢迎标题(英文)",
"settings_home_subtitle_zh": "主页副标题(中文)",
"settings_home_subtitle_en": "主页副标题(英文)",
"settings_footer_text_zh": "页脚文本(中文)",
"settings_footer_text_en": "页脚文本(英文)",
"settings_leave_blank": "留空则使用内置默认值。",
"settings_save": "保存",
"settings_upload_title": "上传设置",
"settings_upload_desc": "附件上传策略与允许的文件类型。",
"settings_uploads_enabled":"启用附件上传",
"settings_default_size": "默认最大大小(MB",
"settings_storage_dir": "存储子目录",
"settings_file_types": "允许的文件类型",
"settings_add_type": "添加文件类型",
"settings_extension": "扩展名",
"settings_mime": "MIME 类型",
"settings_category": "分组",
"settings_max_size": "最大大小(MB",
"settings_enabled": "启用",
"settings_actions": "操作",
"settings_toggle": "切换",
"settings_delete": "删除",
"settings_update": "更新",
"settings_download_title": "下载设置",
"settings_download_desc": "用于拼接附件下载链接的基础地址。",
"settings_add_url": "添加基础地址",
"settings_url_name": "名称",
"settings_base_url": "基础地址",
"settings_priority": "优先级",
"settings_is_default": "默认",
"settings_set_default": "设为默认",
"cat_image": "图片",
"cat_document": "文档",
"cat_archive": "压缩包",
"cat_video": "视频",
"cat_other": "其他",
},
}
// T returns a copy of the translation map for the given language.
// Falls back to English if the language is not supported.
func T(l Lang) map[string]string {
t, ok := translations[l]
if !ok {
t = translations[EN]
}
// Return a shallow copy so callers cannot mutate the original map values.
out := make(map[string]string, len(t))
for k, v := range t {
out[k] = v
}
return out
}
// DetectLang parses the Accept-Language header and returns the best matching
// supported language. Returns EN for unsupported languages.
func DetectLang(acceptHeader string) Lang {
if acceptHeader == "" {
return EN
}
// Accept-Language format: "zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7"
// Split by comma, then extract primary language from each entry.
entries := strings.Split(acceptHeader, ",")
for _, entry := range entries {
// Trim spaces and remove quality values.
entry = strings.TrimSpace(entry)
if idx := strings.Index(entry, ";"); idx != -1 {
entry = entry[:idx]
}
// Extract primary language (before any "-" subtag).
primary := strings.ToLower(entry)
if idx := strings.Index(primary, "-"); idx != -1 {
primary = primary[:idx]
}
switch primary {
case "zh":
return ZH
case "en":
return EN
}
}
return EN
}