diff options
Diffstat (limited to 'FrontEnd/src/i18n')
-rw-r--r-- | FrontEnd/src/i18n/backend.ts | 33 | ||||
-rw-r--r-- | FrontEnd/src/i18n/index.ts | 3 | ||||
-rw-r--r-- | FrontEnd/src/i18n/setup.ts | 50 | ||||
-rw-r--r-- | FrontEnd/src/i18n/text.ts | 35 | ||||
-rw-r--r-- | FrontEnd/src/i18n/translations/en/admin.json | 35 | ||||
-rw-r--r-- | FrontEnd/src/i18n/translations/en/index.json | 228 | ||||
-rw-r--r-- | FrontEnd/src/i18n/translations/zh/admin.json | 35 | ||||
-rw-r--r-- | FrontEnd/src/i18n/translations/zh/index.json | 241 |
8 files changed, 660 insertions, 0 deletions
diff --git a/FrontEnd/src/i18n/backend.ts b/FrontEnd/src/i18n/backend.ts new file mode 100644 index 00000000..92f0c12f --- /dev/null +++ b/FrontEnd/src/i18n/backend.ts @@ -0,0 +1,33 @@ +import { BackendModule } from "i18next"; + + const backend: BackendModule = { + type: "backend", + init() { + /* do nothing */ + }, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async read(language, namespace) { + if (namespace === "translation") { + if (language === "en") { + return await import("./translations/en/index.json"); + } else if (language === "zh") { + return await import("./translations/zh/index.json"); + } else { + throw Error(`Language ${language} is not supported.`); + } + } else if (namespace === "admin") { + if (language === "en") { + return await import("./translations/en/admin.json"); + } else if (language === "zh") { + return await import("./translations/zh/admin.json"); + } else { + throw Error(`Language ${language} is not supported.`); + } + } else { + throw Error(`Namespace ${namespace} is not supported.`); + } + }, +}; + +export default backend; + diff --git a/FrontEnd/src/i18n/index.ts b/FrontEnd/src/i18n/index.ts new file mode 100644 index 00000000..4bd6dc28 --- /dev/null +++ b/FrontEnd/src/i18n/index.ts @@ -0,0 +1,3 @@ +import "./setup"; +export { default as i18n } from "i18next"; +export * from "./text"; diff --git a/FrontEnd/src/i18n/setup.ts b/FrontEnd/src/i18n/setup.ts new file mode 100644 index 00000000..63dd40ed --- /dev/null +++ b/FrontEnd/src/i18n/setup.ts @@ -0,0 +1,50 @@ +import i18n from "i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { initReactI18next } from "react-i18next"; + +import backend from "./backend"; + +void i18n + .use(LanguageDetector) + .use(backend) + .use(initReactI18next) // bind react-i18next to the instance + .init({ + fallbackLng: false, + lowerCaseLng: true, + + debug: process.env.NODE_ENV === "development", + + interpolation: { + escapeValue: false, // not needed for react!! + }, + + // react i18next special options (optional) + // override if needed - omit if ok with defaults + /* + react: { + bindI18n: 'languageChanged', + bindI18nStore: '', + transEmptyNodeValue: '', + transSupportBasicHtmlNodes: true, + transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'], + useSuspense: true, + } + */ + }); + +if (module.hot) { + module.hot.accept( + [ + "./translations/en/index.json", + "./translations/zh/index.json", + "./translations/en/admin.json", + "./translations/zh/admin.json", + ], + () => { + void i18n.reloadResources(); + }, + ); +} + +export default i18n; + diff --git a/FrontEnd/src/i18n/text.ts b/FrontEnd/src/i18n/text.ts new file mode 100644 index 00000000..f8f7e7e6 --- /dev/null +++ b/FrontEnd/src/i18n/text.ts @@ -0,0 +1,35 @@ +import i18n from "i18next"; +import { useTranslation } from "react-i18next"; + +export type I18nText = + | string + | { type: "text" | "custom"; value: string } + | { type: "i18n"; value: string }; + +type T = typeof i18n.t; + +export function convertI18nText(text: I18nText, t: T): string { + if (typeof text === "string") { + return t(text); + } else if (text.type === "i18n") { + return t(text.value); + } else { + return text.value; + } +} + +export interface C { + (text: I18nText): string; +} + +export function createC(t: T): C { + return ((text) => convertI18nText(text, t)) as C; +} + +export const c = createC(i18n.t); + +export function useC(ns?: string): C { + const { t } = useTranslation(ns); + return createC(t); +} + diff --git a/FrontEnd/src/i18n/translations/en/admin.json b/FrontEnd/src/i18n/translations/en/admin.json new file mode 100644 index 00000000..ddb3ffad --- /dev/null +++ b/FrontEnd/src/i18n/translations/en/admin.json @@ -0,0 +1,35 @@ +{
+ "nav": {
+ "users": "Users",
+ "more": "More"
+ },
+ "create": "Create",
+ "user": {
+ "username": "Username: ",
+ "password": "Password: ",
+ "nickname": "Nickname: ",
+ "uniqueId": "Unique ID: ",
+ "permissions": "Permissions: ",
+ "modify": "Modify",
+ "modifyPermissions": "Modify Permissions",
+ "delete": "Delete",
+ "dialog": {
+ "create": {
+ "title": "Create User",
+ "prompt": "You are creating a new user."
+ },
+ "delete": {
+ "title": "Delete user",
+ "prompt": "You are deleting <1>username</1> . Caution: This can't be undo."
+ },
+ "modify": {
+ "title": "Modify User",
+ "prompt": "You are modifying user <1>username</1> ."
+ },
+ "modifyPermissions": {
+ "title": "Modify User Permissions",
+ "prompt": "You are modifying permissions of user <1>username</1> ."
+ }
+ }
+ }
+}
diff --git a/FrontEnd/src/i18n/translations/en/index.json b/FrontEnd/src/i18n/translations/en/index.json new file mode 100644 index 00000000..1b43357c --- /dev/null +++ b/FrontEnd/src/i18n/translations/en/index.json @@ -0,0 +1,228 @@ +{ + "welcome": "Welcome!", + "search": "Search", + "edit": "Edit", + "image": "Image", + "done": "Done", + "preview": "Preview", + "delete": "Delete", + "changeProperty": "Change Property", + "loadFailReload": "Load failed, <1>click here to reload</1>.", + "error": { + "network": "Network error.", + "unknown": "Unknown error." + }, + "connectionState": { + "Connected": "Connected", + "Connecting": "Connecting", + "Disconnected": "Disconnected", + "Disconnecting": "Disconnecting", + "Reconnecting": "Reconnecting" + }, + "visibility": { + "private": "Private For Me", + "register": "Only Registered Users", + "public": "Public To Everyone" + }, + "register": { + "register": "Register", + "username": "Username", + "password": "Password", + "confirmPassword": "Confirm Password", + "registerCode": "Register Code", + "error": { + "usernameEmpty": "Username can't be empty.", + "passwordEmpty": "Password can't be emtpy.", + "confirmPasswordWrong": "Password does not match.", + "registerCodeEmpty": "Register code can't be empty.", + "registerCodeInvalid": "Register code is invalid." + } + }, + "nav": { + "settings": "Settings", + "login": "Login", + "about": "About", + "administration": "Administration" + }, + "chooseImage": "Choose a image", + "loadImageError": "Failed to load image.", + "home": { + "loadingHighlightTimelines": "Loading highlight timelines...", + "loadedHighlightTimelines": "Here are some highlight timelines💡", + "errorHighlightTimelines": "Failed to load highlight timelines, please try reloading!", + "bookmarkTimeline": "Bookmark Timelines", + "highlightTimeline": "Highlight Timelines", + "relatedTimeline": "Timelines You Participate", + "message": { + "moveHighlightFail": "Failed to move highlight timeline.", + "deleteHighlightFail": "Failed to delete highlight timeline.", + "moveBookmarkFail": "Failed to move bookmark timeline.", + "deleteBookmarkFail": "Failed to delete bookmark timeline." + }, + "createButton": "Create Timeline", + "createDialog": { + "title": "Create Timeline!", + "name": "Name", + "nameFormat": "Name must consist of only letter including non-English letter, digit, hyphen(-) and underline(_) and be no longer than 26.", + "badFormat": "Bad format.", + "noEmpty": "Empty is not allowed.", + "tooLong": "Too long." + } + }, + "operationDialog": { + "retry": "Retry", + "nextStep": "Next", + "previousStep": "Previous", + "confirm": "Confirm", + "cancel": "Cancel", + "ok": "OK!", + "processing": "Processing...", + "success": "Success!", + "error": "An error occurred." + }, + "timeline": { + "messageCantSee": "Sorry, you are not allowed to see this timeline.😅", + "userNotExist": "The user does not exist!", + "timelineNotExist": "The timeline does not exist!", + "manage": "Manage", + "memberButton": "Member", + "send": "Send", + "deletePostFailed": "Failed to delete post.", + "sendPostFailed": "Failed to send post.", + "dropDraft": "Drop Draft", + "confirmLeave": "Are you sure to leave? All content you typed would be lost.", + "postNoLogin": "Please <l>login</l> to send post.", + "visibility": { + "public": "public to everyone", + "register": "only registed people can see", + "private": "only members can see" + }, + "visibilityTooltip": { + "public": "Everyone including those without accounts can see content of the timeline.", + "register": "Only those who have an account and logined can see content of the timeline.", + "private": "Only members of this timeline can see content of the timeline." + }, + "dialogChangeProperty": { + "title": "Change Timeline Properties", + "titleField": "Title", + "visibility": "Visibility", + "description": "Description", + "color": "Color" + }, + "changePostPropertyDialog": { + "title": "Change Post Properties", + "time": "Date and time", + "timeEmpty": "You must select a time." + }, + "member": { + "noUserAvailableToAdd": "Sorry, no user available to be a member in search result.", + "add": "Add", + "remove": "Remove" + }, + "manageItem": { + "nickname": "Nickname", + "avatar": "Avatar", + "property": "Timeline Property", + "member": "Timeline Member", + "delete": "Delete Timeline" + }, + "deleteDialog": { + "title": "Delete Timeline", + "inputPrompt": "This is a dangerous action. If you are sure to delete timeline<1>{{name}}</1>, please input its name below and click confirm button.", + "notMatch": "Name does not match." + }, + "post": { + "type": { + "text": "Plain Text", + "markdown": "Markdown", + "image": "Image" + }, + "deleteDialog": { + "title": "Confirm Delete", + "prompt": "Are you sure to delete the post? This operation is not recoverable." + } + }, + "addHighlightFail": "Failed to add highlight.", + "removeHighlightFail": "Failed to remove highlight.", + "addBookmarkFail": "Failed to add bookmark.", + "removeBookmarkFail": "Failed to remove bookmark." + }, + "searchPage": { + "loading": "Loading search result...", + "input": "Input something and search!", + "noResult": "Sorry, there is no satisfied results." + }, + "user": { + "username": "username", + "password": "password", + "login": "login", + "rememberMe": "Remember Me", + "welcomeBack": "Welcome back!", + "tokenInvalid": "Your authentication token is not valid any more. Please log in again!" + }, + "login": { + "emptyUsername": "Username can't be empty.", + "emptyPassword": "Password can't be empty.", + "badCredential": "Username or password is invalid.", + "alreadyLogin": "Already login! Redirect to home page in 3s!", + "noAccount": "If you don't have an account and know a register code, then click <1>here</1> to register." + }, + "settings": { + "subheader": { + "account": "Account", + "customization": "Customization" + }, + "languagePrimary": "Choose display language", + "languageSecondary": "You language preference will be saved locally. Next time you visit this page, last language option will be used.", + "changePassword": "Change account's password", + "logout": "Log out this account", + "changeAvatar": "Change avatar", + "changeNickname": "Change nickname", + "myRegisterCode": "My register code:", + "myRegisterCodeDesc": "Click to create a new register code.", + "renewRegisterCode": "Renew Register Code", + "renewRegisterCodeDesc": "Confirm to renew register code? The old one will no longer be used.", + "myRegisterCodeCopied": "Register code copied!", + "dialogChangePassword": { + "title": "Change Password", + "prompt": "You are changing your password. You need to input the correct old password. After change, you need to login again and all old login will be invalid.", + "inputOldPassword": "Old password", + "inputNewPassword": "New password", + "inputRetypeNewPassword": "Retype new password", + "errorEmptyOldPassword": "Old password can't be empty.", + "errorEmptyNewPassword": "New password can't be empty.", + "errorRetypeNotMatch": "Password retyped does not match." + }, + "dialogConfirmLogout": { + "title": "Confirm Logout", + "prompt": "Are you sure to log out? All cached data in the browser will be deleted." + }, + "dialogChangeNickname": { + "title": "Change Nickname", + "inputLabel": "New nickname" + }, + "dialogChangeAvatar": { + "title": "Change Avatar", + "previewImgAlt": "preview", + "prompt": { + "select": "Please select a picture.", + "crop": "Please crop the picture.", + "processingCrop": "Cropping picture...", + "uploading": "Uploading...", + "preview": "Please preview avatar" + }, + "upload": "upload" + } + }, + "about": { + "credits": { + "title": "Credits", + "content": "Timeline stands on shoulders of giants. Special appreciation for many open source projects listed below or not. Related licenses could be found in GitHub repository.", + "frontend": "Frontend", + "backend": "Backend" + } + }, + "admin": { + "title": "admin" + } +} diff --git a/FrontEnd/src/i18n/translations/zh/admin.json b/FrontEnd/src/i18n/translations/zh/admin.json new file mode 100644 index 00000000..edd1cabd --- /dev/null +++ b/FrontEnd/src/i18n/translations/zh/admin.json @@ -0,0 +1,35 @@ +{
+ "nav": {
+ "users": "用户",
+ "more": "更多"
+ },
+ "create": "创建",
+ "user": {
+ "username": "用户名:",
+ "password": "密码:",
+ "nickname": "昵称:",
+ "uniqueId": "唯一ID:",
+ "permissions": "权限:",
+ "modify": "修改",
+ "modifyPermissions": "修改权限",
+ "delete": "删除",
+ "dialog": {
+ "create": {
+ "title": "创建用户",
+ "prompt": "您正在创建一个新用户。"
+ },
+ "delete": {
+ "title": "删除用户",
+ "prompt": "您正在删除用户 <1>username</1> 。注意:此操作不可撤销。"
+ },
+ "modify": {
+ "title": "修改用户",
+ "prompt": "您正在修改用户 <1>username</1> 。"
+ },
+ "modifyPermissions": {
+ "title": "修改用户权限",
+ "prompt": "您正在修改用户 <1>username</1> 的权限。"
+ }
+ }
+ }
+}
diff --git a/FrontEnd/src/i18n/translations/zh/index.json b/FrontEnd/src/i18n/translations/zh/index.json new file mode 100644 index 00000000..dc0d6672 --- /dev/null +++ b/FrontEnd/src/i18n/translations/zh/index.json @@ -0,0 +1,241 @@ +{ + "welcome": "欢迎!", + "search": "搜索", + "edit": "编辑", + "image": "图片", + "done": "完成", + "preview": "预览", + "loadFailReload": "加载失败,<1>点击重试</1>。", + "delete": "删除", + "changeProperty": "修改属性", + "error": { + "network": "网络错误。", + "unknown": "未知错误。" + }, + "visibility": { + "private": "仅自己可见", + "register": "仅注册用户可见", + "public": "对所有人公开" + }, + "register": { + "register": "注册", + "username": "用户名", + "password": "密码", + "confirmPassword": "确认密码", + "registerCode": "注册码", + "error": { + "usernameEmpty": "用户名不能为空。", + "passwordEmpty": "密码不能为空。", + "confirmPasswordWrong": "密码不匹配。", + "registerCodeEmpty": "注册码不能为空。", + "registerCodeInvalid": "注册码无效。" + } + }, + "connectionState": { + "Connected": "已连接", + "Connecting": "正在连接", + "Disconnected": "已断开连接", + "Disconnecting": "正在断开连接", + "Reconnecting": "正在重新连接" + }, + "nav": { + "settings": "设置", + "login": "登陆", + "about": "关于", + "administration": "管理" + }, + "chooseImage": "选择一个图片", + "loadImageError": "加载图片失败", + "home": { + "loadingHighlightTimelines": "正在加载高光时间线...", + "loadedHighlightTimelines": "康康以下这些高光时间线💡", + "errorHighlightTimelines": "加载高光时间线失败,刷新试试!", + "bookmarkTimeline": "书签时间线", + "highlightTimeline": "高光时间线", + "relatedTimeline": "参与的时间线", + "message": { + "moveHighlightFail": "移动高光时间线失败。", + "deleteHighlightFail": "删除高光时间线失败。", + "moveBookmarkFail": "移动书签时间线失败。", + "deleteBookmarkFail": "删除书签时间线失败。" + }, + "createButton": "创建时间线", + "createDialog": { + "title": "创建时间线!", + "name": "名字", + "nameFormat": "名字只能由字母、汉字、数字、下划线(_)和连字符(-)构成,且长度不能超过26.", + "badFormat": "格式错误", + "noEmpty": "不能为空", + "tooLong": "太长了" + } + }, + "operationDialog": { + "retry": "重试", + "nextStep": "下一步", + "previousStep": "上一步", + "confirm": "确定", + "cancel": "取消", + "ok": "好的!", + "processing": "处理中...", + "success": "成功!", + "error": "出错啦!" + }, + "timeline": { + "messageCantSee": "不好意思,你没有权限查看这个时间线。😅", + "userNotExist": "该用户不存在!", + "timelineNotExist": "该时间线不存在!", + "manage": "管理", + "memberButton": "成员", + "send": "发送", + "deletePostFailed": "删除消息失败。", + "sendPostFailed": "发送消息失败。", + "dropDraft": "放弃草稿", + "confirmLeave": "确定要离开吗?所有输入的内容将会丢失。", + "postNoLogin": "<l>登陆</l>后即可发表消息!", + "visibility": { + "public": "对所有人公开", + "register": "仅注册可见", + "private": "仅成员可见" + }, + "visibilityTooltip": { + "public": "所有人都可以看到这个时间线的内容,包括没有注册的人。", + "register": "只有拥有本网站的账号且登陆了的人才能看到这个时间线的内容。", + "private": "只有这个时间线的成员可以看到这个时间线的内容。" + }, + "dialogChangeProperty": { + "title": "修改时间线属性", + "titleField": "标题", + "visibility": "可见性", + "description": "描述", + "color": "颜色" + }, + "changePostPropertyDialog": { + "title": "修改消息属性", + "time": "时间", + "timeEmpty": "你必须选择一个时间。" + }, + "member": { + "noUserAvailableToAdd": "搜索结果显示没有可以添加为成员的用户。", + "add": "添加", + "remove": "移除" + }, + "manageItem": { + "nickname": "昵称", + "avatar": "头像", + "property": "时间线属性", + "member": "时间线成员", + "delete": "删除时间线" + }, + "deleteDialog": { + "title": "删除时间线", + "inputPrompt": "这是一个危险的操作。如果您确认要删除时间线<1>{{name}}</1>,请在下面输入它的名字并点击确认。", + "notMatch": "名字不匹配" + }, + "post": { + "type": { + "text": "纯文本", + "markdown": "Markdown", + "image": "图片" + }, + "deleteDialog": { + "title": "确认删除", + "prompt": "确定删除这个消息?这个操作不可撤销。" + } + }, + "addHighlightFail": "添加高光失败。", + "removeHighlightFail": "删除高光失败。", + "addBookmarkFail": "添加书签失败。", + "removeBookmarkFail": "删除书签失败。" + }, + "searchPage": { + "loading": "加载搜索结果中...", + "input": "输入一些东西来搜索!", + "noResult": "对不起,没有符合条件的结果。" + }, + "user": { + "username": "用户名", + "password": "密码", + "login": "登录", + "rememberMe": "记住我", + "welcomeBack": "欢迎回来!", + "tokenInvalid": "您的登录信息已失效,请重新登陆!" + }, + "login": { + "emptyUsername": "用户名不能为空。", + "emptyPassword": "密码不能为空。", + "badCredential": "用户名或密码错误。", + "alreadyLogin": "已经登陆,三秒后导航到首页!", + "noAccount": "如果你没有账号但有一个注册码,请点击<1>这里</1>注册账号。" + }, + "settings": { + "subheader": { + "account": "账户", + "customization": "个性化" + }, + "languagePrimary": "选择显示的语言。", + "languageSecondary": "您的语言偏好将会存储在本地,下次浏览时将自动使用上次保存的语言选项。", + "changePassword": "更改账号的密码", + "logout": "注销此账号", + "changeAvatar": "更改头像", + "changeNickname": "更改昵称", + "changeBookmarkVisibility": "修改书签时间线可见性", + "myRegisterCode": "我的注册码:", + "myRegisterCodeDesc": "点击以创建新的注册码。", + "renewRegisterCode": "创建新的注册码", + "renewRegisterCodeDesc": "确定要创建新的注册码吗?旧的注册码将无法再使用。", + "myRegisterCodeCopied": "注册码已复制!", + "dialogChangePassword": { + "title": "修改密码", + "prompt": "您正在修改密码,您需要输入正确的旧密码。成功修改后您需要重新登陆,而且以前所有的登录都会失效。", + "inputOldPassword": "旧密码", + "inputNewPassword": "新密码", + "inputRetypeNewPassword": "再次输入新密码", + "errorEmptyOldPassword": "旧密码不能为空。", + "errorEmptyNewPassword": "新密码不能为空", + "errorRetypeNotMatch": "两次输入的密码不一致" + }, + "dialogConfirmLogout": { + "title": "确定注销", + "prompt": "您确定注销此账号?这将删除所有已经缓存在浏览器的数据。" + }, + "dialogChangeNickname": { + "title": "更改昵称", + "inputLabel": "新昵称" + }, + "dialogChangeAvatar": { + "title": "修改头像", + "previewImgAlt": "预览", + "prompt": { + "select": "请选择一个图片", + "crop": "请裁剪图片", + "processingCrop": "正在裁剪图片", + "uploading": "正在上传", + "preview": "请预览图片" + }, + "upload": "上传" + } + }, + "about": { + "author": { + "title": "网站作者", + "name": "名字:", + "introduction": "简介:", + "introductionContent": "一个基于巧合编程的代码爱好者。", + "links": "链接:" + }, + "site": { + "title": "网站信息", + "content": "这个网站的名字叫 <1>Timeline</1>,是一个以<3>时间线</3>为核心概念的 Web App . 它的前端和后端都是由<5>我</5>开发,并且在 GitHub 上开源。大家可以相对轻松的把它们部署在自己的服务器上,这也是我的目标之一。欢迎大家前往 GitHub 仓库提出任何意见。", + "repo": "GitHub 仓库" + }, + "credits": { + "title": "鸣谢", + "content": "Timeline 是站在巨人肩膀上的作品,感谢以下列出的和其他未列出的许多开源项目,相关 License 请在 GitHub 仓库中查看。", + "frontend": "前端:", + "backend": "后端:" + } + }, + "admin": { + "title": "管理" + } +} |