From cd1a848b459e6e4ea6a7f4e3510aadd307431462 Mon Sep 17 00:00:00 2001 From: lleohao <12764126+lleohao@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:05:03 +0800 Subject: [PATCH] feat: support i18n --- apps/client/README.md | 6 + apps/client/package.json | 4 + .../public/locales/en/invite-signup.json | 11 + apps/client/public/locales/en/login.json | 7 + apps/client/public/locales/en/settings.json | 138 + .../public/locales/en/setup-workspace.json | 11 + .../client/public/locales/en/translation.json | 55 + .../public/locales/zh/invite-signup.json | 11 + apps/client/public/locales/zh/login.json | 7 + apps/client/public/locales/zh/settings.json | 138 + .../public/locales/zh/setup-workspace.json | 11 + .../client/public/locales/zh/translation.json | 55 + apps/client/scripts/i18n-tools.js | 59 + apps/client/src/App.tsx | 8 +- .../src/components/common/recent-changes.tsx | 9 +- .../components/layouts/global/app-header.tsx | 6 +- .../components/layouts/global/top-menu.tsx | 18 +- .../components/settings/settings-sidebar.tsx | 10 +- .../auth/components/invite-sign-up-form.tsx | 18 +- .../features/auth/components/login-form.tsx | 12 +- .../auth/components/setup-workspace-form.tsx | 20 +- .../components/add-group-member-modal.tsx | 12 +- .../group/components/create-group-form.tsx | 16 +- .../group/components/create-group-modal.tsx | 8 +- .../group/components/edit-group-form.tsx | 14 +- .../group/components/edit-group-modal.tsx | 7 +- .../group/components/group-action-menu.tsx | 17 +- .../group/components/group-details.tsx | 1 + .../features/group/components/group-list.tsx | 8 +- .../group/components/group-members.tsx | 21 +- .../group/components/multi-user-select.tsx | 10 +- .../features/home/components/home-tabs.tsx | 5 +- .../page-history/components/history-list.tsx | 22 +- .../page-history/components/history-modal.tsx | 4 +- .../page-history/components/history-view.tsx | 18 +- .../components/header/page-header-menu.tsx | 17 +- .../components/add-space-members-modal.tsx | 12 +- .../space/components/create-space-form.tsx | 18 +- .../space/components/create-space-modal.tsx | 8 +- .../space/components/edit-space-form.tsx | 16 +- .../space/components/multi-member-select.tsx | 8 +- .../space/components/settings-modal.tsx | 8 +- .../space/components/space-details.tsx | 6 +- .../features/space/components/space-grid.tsx | 5 +- .../space/components/space-home-tabs.tsx | 6 +- .../features/space/components/space-list.tsx | 8 +- .../space/components/space-members.tsx | 19 +- .../user/components/account-avatar.tsx | 6 +- .../user/components/account-languate.tsx | 44 + .../user/components/account-name-form.tsx | 14 +- .../user/components/account-theme.tsx | 21 +- .../features/user/components/change-email.tsx | 29 +- .../user/components/change-password.tsx | 36 +- .../user/components/page-width-pref.tsx | 14 +- .../components/workspace-invite-form.tsx | 26 +- .../components/workspace-invite-modal.tsx | 8 +- .../components/workspace-invites-table.tsx | 14 +- .../components/workspace-members-table.tsx | 16 +- .../components/workspace-name-form.tsx | 14 +- apps/client/src/i18n.ts | 34 + apps/client/src/main.tsx | 6 +- apps/client/src/pages/auth/invite-signup.tsx | 5 +- apps/client/src/pages/auth/login.tsx | 5 +- .../client/src/pages/auth/setup-workspace.tsx | 4 +- apps/client/src/pages/page/page.tsx | 6 +- .../settings/account/account-preferences.tsx | 10 +- .../settings/account/account-settings.tsx | 7 +- .../src/pages/settings/group/group-info.tsx | 7 +- .../src/pages/settings/group/groups.tsx | 6 +- .../src/pages/settings/space/spaces.tsx | 6 +- .../settings/workspace/workspace-members.tsx | 10 +- .../settings/workspace/workspace-settings.tsx | 7 +- package.json | 3 + pnpm-lock.yaml | 18351 ++++++++++------ 74 files changed, 12842 insertions(+), 6775 deletions(-) create mode 100644 apps/client/public/locales/en/invite-signup.json create mode 100644 apps/client/public/locales/en/login.json create mode 100644 apps/client/public/locales/en/settings.json create mode 100644 apps/client/public/locales/en/setup-workspace.json create mode 100644 apps/client/public/locales/en/translation.json create mode 100644 apps/client/public/locales/zh/invite-signup.json create mode 100644 apps/client/public/locales/zh/login.json create mode 100644 apps/client/public/locales/zh/settings.json create mode 100644 apps/client/public/locales/zh/setup-workspace.json create mode 100644 apps/client/public/locales/zh/translation.json create mode 100644 apps/client/scripts/i18n-tools.js create mode 100644 apps/client/src/features/user/components/account-languate.tsx create mode 100644 apps/client/src/i18n.ts diff --git a/apps/client/README.md b/apps/client/README.md index 1ebe379f..02db9c0f 100644 --- a/apps/client/README.md +++ b/apps/client/README.md @@ -25,3 +25,9 @@ If you are developing a production application, we recommend updating the config - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list + +## Add i18n namespaces + +`node scripts/i18n-tools.js add-ns ` + +For example `node scripts/i18n-tools.js add-ns login` diff --git a/apps/client/package.json b/apps/client/package.json index 83d40791..563b64b7 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -27,6 +27,9 @@ "date-fns": "^3.6.0", "emoji-mart": "^5.6.0", "file-saver": "^2.0.5", + "i18next": "^23.14.0", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.1", "jotai": "^2.9.3", "jotai-optics": "^0.4.0", "js-cookie": "^3.0.5", @@ -41,6 +44,7 @@ "react-drawio": "^0.2.0", "react-error-boundary": "^4.0.13", "react-helmet-async": "^2.0.5", + "react-i18next": "^15.0.1", "react-moveable": "^0.56.0", "react-router-dom": "^6.26.1", "socket.io-client": "^4.7.5", diff --git a/apps/client/public/locales/en/invite-signup.json b/apps/client/public/locales/en/invite-signup.json new file mode 100644 index 00000000..921066ec --- /dev/null +++ b/apps/client/public/locales/en/invite-signup.json @@ -0,0 +1,11 @@ +{ + "Join the workspace": "Join the workspace", + "Name": "Name", + "enter your full name": "enter your full name", + "Email": "Email", + "Password": "Password", + "Your password": "Your password", + "Sign Up": "Sign Up", + "invalid invitation link": "invalid invitation link", + "Invitation signup": "Invitation signup" +} diff --git a/apps/client/public/locales/en/login.json b/apps/client/public/locales/en/login.json new file mode 100644 index 00000000..d5e6319f --- /dev/null +++ b/apps/client/public/locales/en/login.json @@ -0,0 +1,7 @@ +{ + "Login": "Login", + "Email": "Email", + "Password": "Password", + "Your password": "Your password", + "Sign In": "Sign In" +} diff --git a/apps/client/public/locales/en/settings.json b/apps/client/public/locales/en/settings.json new file mode 100644 index 00000000..a1e19862 --- /dev/null +++ b/apps/client/public/locales/en/settings.json @@ -0,0 +1,138 @@ +{ + "account": { + "My Profile": "My Profile", + "Change photo": "Change photo", + "Name": "Name", + "Your name": "Your name", + "Save": "Save", + "Updated successfully": "Updated successfully", + "Failed to update data": "Failed to update data", + "Email": "Email", + "Change email": "Change email", + "To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.", + "Password": "Password", + "Enter your password": "Enter your password", + "Enter your new preferred email": "Enter your new preferred email", + "New email": "New email", + "You can change your password here.": "You can change your password here.", + "Change password": "Change password", + "Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.", + "Current password": "Current password", + "Enter your current password": "Enter your current password", + "New password": "New password", + "Enter your new password": "Enter your new password", + "Password changed successfully": "Password changed successfully" + }, + "preference": { + "Preferences": "Preferences", + "Theme": "Theme", + "Choose your preferred color scheme.": "Choose your preferred color scheme.", + "Select theme": "Select theme", + "Light": "Light", + "Dark": "Dark", + "System settings": "System settings", + "Language": "Language", + "Choose your preferred interface language.": "Choose your preferred interface language.", + "Select language": "Select language", + "Full page width": "Full page width", + "Choose your preferred page width.": "Choose your preferred page width.", + "Toggle full page width": "Toggle full page width" + }, + "workspace": { + "general": { + "General": "General", + "Name": "Name", + "e.g ACME": "e.g ACME", + "Save": "Save", + "Updated successfully": "Updated successfully", + "Failed to update data": "Failed to update data" + }, + "member": { + "Members": "Members", + "Invite members": "Invite members", + "Invite new members": "Invite new members", + "Pending": "Pending", + "User": "User", + "Status": "Status", + "Role": "Role", + "Active": "Active", + "Email": "Email", + "Date": "Date", + "Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.", + "Invite by email": "Invite by email", + "Enter valid email addresses separated by comma or space max_50": "Enter valid email addresses separated by comma or space [max: 50]", + "enter valid emails addresses": "enter valid emails addresses", + "Select role": "Select role", + "Select role to assign to all invited members": "Select role to assign to all invited members", + "Choose a role": "Choose a role", + "Add to groups": "Add to groups", + "Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access", + "Send invitation": "Send invitation" + }, + "group": { + "Groups": "Groups", + "Create group": "Create group", + "Group": "Group", + "Members": "Members", + "member": "member", + "members": "members", + "Manage Group": "Manage Group", + "addGroupMembers": "addGroupMembers", + "add": "add", + "Edit group": "Edit group", + "Group name": "Group name", + "e.g Developers": "e.g Developers", + "Group description": "Group description", + "e.g Group for developers": "e.g Group for developers", + "Edit": "Edit", + "Delete group": "Delete group", + "Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.", + "Delete": "Delete", + "Cancel": "Cancel", + "Remove group member": "Remove group member", + "Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.", + "Add group members": "Add group members", + "Create": "Create", + "User": "User", + "Status": "Status", + "Active": "Active", + "Add members": "Add members", + "Search for users": "Search for users", + "No user found": "No user found" + }, + "space": { + "Spaces": "Spaces", + "Create space": "Create space", + "Space": "Space", + "Members": "Members", + "Settings": "Settings", + "Details": "Details", + "Name": "Name", + "e.g Sales": "e.g Sales", + "Slug": "Slug", + "Description": "Description", + "e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate", + "Save": "Save", + "Space name": "Space name", + "e.g Product Team": "e.g Product Team", + "Space slug": "Space slug", + "e.g product": "e.g product", + "Space description": "Space description", + "e.g Space for product team": "e.g Space for product team", + "Create": "Create", + "addSpaceMembers": "addSpaceMembers", + "add": "add", + "selectRole": "selectRole", + "Remove space member": "Remove space member", + "Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Are you sure you want to remove this user from the space? The user will lose all access to this space.", + "Remove": "Remove", + "Cancel": "Cancel", + "Member": "Member", + "Role": "Role", + "Failed to load page. An error occurred.": "Failed to load page. An error occurred.", + "Add members": "Add members", + "Search for users and groups": "Search for users and groups", + "No user found": "No user found" + } + } +} diff --git a/apps/client/public/locales/en/setup-workspace.json b/apps/client/public/locales/en/setup-workspace.json new file mode 100644 index 00000000..b1d15e48 --- /dev/null +++ b/apps/client/public/locales/en/setup-workspace.json @@ -0,0 +1,11 @@ +{ + "Setup workspace": "Setup workspace", + "Create workspace": "Create workspace", + "Workspace Name": "Workspace Name", + "e.g ACME Inc": "e.g ACME Inc", + "Your Name": "Your Name", + "enter your full name": "enter your full name", + "Your Email": "Your Email", + "Password": "Password", + "Enter a strong password": "Enter a strong password" +} diff --git a/apps/client/public/locales/en/translation.json b/apps/client/public/locales/en/translation.json new file mode 100644 index 00000000..2ae48360 --- /dev/null +++ b/apps/client/public/locales/en/translation.json @@ -0,0 +1,55 @@ +{ + "common": { + "Failed to fetch recent pages": "Failed to fetch recent pages", + "Untitled": "Untitled", + "No pages yet": "No pages yet", + "Failed to load page. An error occurred.": "Failed to load page. An error occurred." + }, + "layout": { + "Home": "Home", + "Workspace": "Workspace", + "Workspace settings": "Workspace settings", + "Manage members": "Manage members", + "Account": "Account", + "My profile": "My profile", + "My preferences": "My preferences", + "Logout": "Logout", + "Settings": "Settings", + "Profile": "Profile", + "Preferences": "Preferences", + "General": "General", + "Members": "Members", + "Groups": "Groups", + "Spaces": "Spaces" + }, + "home": { + "Recently updated": "Recently updated" + }, + "space": { + "Spaces you belong to": "Spaces you belong to", + "Recently updated": "Recently updated" + }, + "page": { + "Error fetching page data.": "Error fetching page data.", + "untitled": "untitled", + "Link copied": "Link copied", + "Copy link": "Copy link", + "Full width": "Full width", + "Page history": "Page history", + "Export": "Export", + "Print PDF": "Print PDF", + "Delete": "Delete" + }, + "page-history": { + "Page history": "Page history", + "Error loading page history.": "Error loading page history.", + "No page history saved yet.": "No page history saved yet.", + "Please confirm your action": "Please confirm your action", + "Are you sure you want to restore this version? Any changes not versioned will be lost.": "Are you sure you want to restore this version? Any changes not versioned will be lost.", + "Confirm": "Confirm", + "Cancel": "Cancel", + "Successfully restored": "Successfully restored", + "Restore": "Restore", + "Error fetching page data.": "Error fetching page data." + } +} diff --git a/apps/client/public/locales/zh/invite-signup.json b/apps/client/public/locales/zh/invite-signup.json new file mode 100644 index 00000000..42af9a04 --- /dev/null +++ b/apps/client/public/locales/zh/invite-signup.json @@ -0,0 +1,11 @@ +{ + "Join the workspace": "加入工作空间", + "Name": "姓名", + "enter your full name": "输入您的全名", + "Email": "电子邮箱", + "Password": "密码", + "Your password": "您的密码", + "Sign Up": "注册", + "invalid invitation link": "无效的邀请链接", + "Invitation signup": "邀请注册" +} diff --git a/apps/client/public/locales/zh/login.json b/apps/client/public/locales/zh/login.json new file mode 100644 index 00000000..3d17499a --- /dev/null +++ b/apps/client/public/locales/zh/login.json @@ -0,0 +1,7 @@ +{ + "Login": "登录", + "Email": "电子邮箱", + "Password": "密码", + "Your password": "您的密码", + "Sign In": "登录" +} diff --git a/apps/client/public/locales/zh/settings.json b/apps/client/public/locales/zh/settings.json new file mode 100644 index 00000000..ed208ff5 --- /dev/null +++ b/apps/client/public/locales/zh/settings.json @@ -0,0 +1,138 @@ +{ + "account": { + "My Profile": "我的个人资料", + "Change photo": "更改照片", + "Name": "姓名", + "Your name": "您的姓名", + "Save": "保存", + "Updated successfully": "更新成功", + "Failed to update data": "数据更新失败", + "Email": "电子邮箱", + "Change email": "更改电子邮箱", + "To change your email, you have to enter your password and new email.": "要更改您的电子邮箱,您需要输入密码和新的电子邮箱地址。", + "Password": "密码", + "Enter your password": "输入您的密码", + "Enter your new preferred email": "输入您新的首选电子邮箱", + "New email": "新电子邮箱", + "You can change your password here.": "您可以在这里更改密码。", + "Change password": "更改密码", + "Your password must be a minimum of 8 characters.": "您的密码必须至少包含8个字符。", + "Current password": "当前密码", + "Enter your current password": "输入您的当前密码", + "New password": "新密码", + "Enter your new password": "输入您的新密码", + "Password changed successfully": "密码更改成功" + }, + "preference": { + "Preferences": "偏好设置", + "Theme": "主题", + "Choose your preferred color scheme.": "选择您喜欢的配色方案。", + "Select theme": "选择主题", + "Light": "浅色", + "Dark": "深色", + "System settings": "系统设置", + "Language": "语言", + "Choose your preferred interface language.": "选择您喜欢的界面语言。", + "Select language": "选择语言", + "Full page width": "全页宽度", + "Choose your preferred page width.": "选择您喜欢的页面宽度。", + "Toggle full page width": "切换全页宽度" + }, + "workspace": { + "general": { + "General": "常规", + "Name": "名称", + "e.g ACME": "例如:ACME", + "Save": "保存", + "Updated successfully": "更新成功", + "Failed to update data": "数据更新失败" + }, + "member": { + "Members": "成员", + "Invite members": "邀请成员", + "Invite new members": "邀请新成员", + "Pending": "待定", + "User": "用户", + "Status": "状态", + "Role": "角色", + "Active": "活跃", + "Email": "电子邮箱", + "Date": "日期", + "Invited members who are yet to accept their invitation will appear here.": "尚未接受邀请的成员将显示在这里。", + "Invite by email": "通过电子邮箱邀请", + "Enter valid email addresses separated by comma or space max_50": "输入有效的电子邮箱地址,用逗号或空格分隔 [最多:50个]", + "enter valid emails addresses": "输入有效的电子邮箱地址", + "Select role": "选择角色", + "Select role to assign to all invited members": "选择要分配给所有被邀请成员的角色", + "Choose a role": "选择一个角色", + "Add to groups": "添加到群组", + "Invited members will be granted access to spaces the groups can access": "被邀请的成员将被授予访问群组可以访问的空间的权限", + "Send invitation": "发送邀请" + }, + "group": { + "Groups": "群组", + "Create group": "创建群组", + "Group": "群组", + "Members": "成员", + "member": "成员", + "members": "成员", + "Manage Group": "管理群组", + "addGroupMembers": "添加群组成员", + "add": "添加", + "Edit group": "编辑群组", + "Group name": "群组名称", + "e.g Developers": "例如:开发人员", + "Group description": "群组描述", + "e.g Group for developers": "例如:开发人员群组", + "Edit": "编辑", + "Delete group": "删除群组", + "Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "您确定要删除这个群组吗?成员将失去对该群组可访问资源的访问权限。", + "Delete": "删除", + "Cancel": "取消", + "Remove group member": "移除群组成员", + "Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "您确定要从群组中移除这个用户吗?该用户将失去对该群组可访问资源的访问权限。", + "Add group members": "添加群组成员", + "Create": "创建", + "User": "用户", + "Status": "状态", + "Active": "活跃", + "Add members": "添加成员", + "Search for users": "搜索用户", + "No user found": "未找到用户" + }, + "space": { + "Spaces": "空间", + "Create space": "创建空间", + "Space": "空间", + "Members": "成员", + "Settings": "设置", + "Details": "详情", + "Name": "名称", + "e.g Sales": "例如:销售", + "Slug": "短链接", + "Description": "描述", + "e.g Space for sales team to collaborate": "例如:销售团队协作的空间", + "Save": "保存", + "Space name": "空间名称", + "e.g Product Team": "例如:产品团队", + "Space slug": "空间短链接", + "e.g product": "例如:product", + "Space description": "空间描述", + "e.g Space for product team": "例如:产品团队的空间", + "Create": "创建", + "addSpaceMembers": "添加空间成员", + "add": "添加", + "selectRole": "选择角色", + "Remove space member": "移除空间成员", + "Are you sure you want to remove this user from the space? The user will lose all access to this space.": "您确定要从空间中移除这个用户吗?该用户将失去对这个空间的所有访问权限。", + "Remove": "移除", + "Cancel": "取消", + "Member": "成员", + "Role": "角色", + "Failed to load page. An error occurred.": "页面加载失败。发生了一个错误。", + "Add members": "添加成员", + "Search for users and groups": "搜索用户和群组", + "No user found": "未找到用户" + } + } +} diff --git a/apps/client/public/locales/zh/setup-workspace.json b/apps/client/public/locales/zh/setup-workspace.json new file mode 100644 index 00000000..7943499d --- /dev/null +++ b/apps/client/public/locales/zh/setup-workspace.json @@ -0,0 +1,11 @@ +{ + "Setup workspace": "设置工作空间", + "Create workspace": "创建工作空间", + "Workspace Name": "工作空间名称", + "e.g ACME Inc": "例如:ACME Inc", + "Your Name": "您的姓名", + "enter your full name": "输入您的全名", + "Your Email": "您的电子邮箱", + "Password": "密码", + "Enter a strong password": "输入一个强密码" +} diff --git a/apps/client/public/locales/zh/translation.json b/apps/client/public/locales/zh/translation.json new file mode 100644 index 00000000..61aab577 --- /dev/null +++ b/apps/client/public/locales/zh/translation.json @@ -0,0 +1,55 @@ +{ + "common": { + "Failed to fetch recent pages": "获取最近页面失败", + "Untitled": "无标题", + "No pages yet": "暂无页面", + "Failed to load page. An error occurred.": "加载页面失败。发生错误。" + }, + "layout": { + "Home": "首页", + "Workspace": "工作区", + "Workspace settings": "工作区设置", + "Manage members": "管理成员", + "Account": "账户", + "My profile": "我的个人资料", + "My preferences": "我的偏好设置", + "Logout": "退出登录", + "Settings": "设置", + "Profile": "个人资料", + "Preferences": "偏好设置", + "General": "常规", + "Members": "成员", + "Groups": "群组", + "Spaces": "空间" + }, + "home": { + "Recently updated": "最近更新" + }, + "space": { + "Spaces you belong to": "您所属的空间", + "Recently updated": "最近更新" + }, + "page": { + "Error fetching page data.": "获取页面数据时出错。", + "untitled": "无标题", + "Link copied": "链接已复制", + "Copy link": "复制链接", + "Full width": "全宽", + "Page history": "页面历史", + "Export": "导出", + "Print PDF": "打印 PDF", + "Delete": "删除" + }, + "page-history": { + "Page history": "页面历史", + "Error loading page history.": "加载页面历史时出错。", + "No page history saved yet.": "尚未保存页面历史。", + "Please confirm your action": "请确认您的操作", + "Are you sure you want to restore this version? Any changes not versioned will be lost.": "您确定要恢复此版本吗?任何未版本化的更改将会丢失。", + "Confirm": "确认", + "Cancel": "取消", + "Successfully restored": "恢复成功", + "Restore": "恢复", + "Error fetching page data.": "获取页面数据时出错。" + } +} diff --git a/apps/client/scripts/i18n-tools.js b/apps/client/scripts/i18n-tools.js new file mode 100644 index 00000000..33862824 --- /dev/null +++ b/apps/client/scripts/i18n-tools.js @@ -0,0 +1,59 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const localsPath = path.join(__dirname, "../public/locales"); + +const supportLanguages = ["en", "zh"]; +const supportCommands = ["add-ns"]; + +function ensureLanguageDirectories() { + if (!fs.existsSync(localsPath)) { + fs.mkdirSync(localsPath, { recursive: true }); + } + + supportLanguages.forEach((lang) => { + const langPath = path.join(localsPath, lang); + + if (!fs.existsSync(langPath)) { + fs.mkdirSync(langPath); + } + }); +} + +function addNamespaces(namespaces) { + supportLanguages.forEach((lang) => { + const langPath = path.join(localsPath, lang); + + namespaces.forEach((ns) => { + const nsFilePath = path.join(langPath, `${ns}.json`); + + if (!fs.existsSync(nsFilePath)) { + fs.writeFileSync(nsFilePath, "{}", "utf8"); + console.log(`Created empty ${ns}.json file in ${lang} directory`); + } else { + console.log( + `${ns}.json file already exists in ${lang} directory, skipping creation`, + ); + } + }); + }); +} + +ensureLanguageDirectories(); + +const [command, ...params] = process.argv.slice(2); + +if (!supportCommands.includes(command)) { + console.warn( + `Only support the follow commands: ${supportCommands.join(" ")} `, + ); + console.log(); +} + +switch (command) { + case "add-ns": + addNamespaces(params); + break; + default: + console.warn("You should input a command"); +} diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index f4ac926e..79e23f97 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -14,7 +14,7 @@ import { useQuerySubscription } from "@/features/websocket/use-query-subscriptio import { useAtom, useAtomValue } from "jotai"; import { socketAtom } from "@/features/websocket/atoms/socket-atom.ts"; import { useTreeSocket } from "@/features/websocket/use-tree-socket.ts"; -import { useEffect } from "react"; +import { useEffect, useTransition } from "react"; import { io } from "socket.io-client"; import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom.ts"; import { SOCKET_URL } from "@/features/websocket/types"; @@ -24,10 +24,14 @@ import PageRedirect from "@/pages/page/page-redirect.tsx"; import Layout from "@/components/layouts/global/layout.tsx"; import { ErrorBoundary } from "react-error-boundary"; import InviteSignup from "@/pages/auth/invite-signup.tsx"; +import { useTranslation } from "react-i18next"; export default function App() { const [, setSocket] = useAtom(socketAtom); const authToken = useAtomValue(authTokensAtom); + const { t } = useTranslation("translation", { + keyPrefix: "common", + }); useEffect(() => { if (!authToken?.accessToken) { @@ -74,7 +78,7 @@ export default function App() { path={"/s/:spaceSlug/p/:pageSlug"} element={ Failed to load page. An error occurred.} + fallback={<>{t("Failed to load page. An error occurred.")}} > diff --git a/apps/client/src/components/common/recent-changes.tsx b/apps/client/src/components/common/recent-changes.tsx index 1fe8050e..f99e6476 100644 --- a/apps/client/src/components/common/recent-changes.tsx +++ b/apps/client/src/components/common/recent-changes.tsx @@ -13,11 +13,14 @@ import { formattedDate } from "@/lib/time.ts"; import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts"; import { IconFileDescription } from "@tabler/icons-react"; import { getSpaceUrl } from "@/lib/config.ts"; +import { useTranslation } from "react-i18next"; interface Props { spaceId?: string; } export default function RecentChanges({ spaceId }: Props) { + const { t } = useTranslation("translation", { keyPrefix: "common" }); + const { data: pages, isLoading, isError } = useRecentChangesQuery(spaceId); if (isLoading) { @@ -25,7 +28,7 @@ export default function RecentChanges({ spaceId }: Props) { } if (isError) { - return Failed to fetch recent pages; + return {t("Failed to fetch recent pages")}; } return pages && pages.items.length > 0 ? ( @@ -43,7 +46,7 @@ export default function RecentChanges({ spaceId }: Props) { {page.icon || } - {page.title || "Untitled"} + {page.title || t("Untitled")} @@ -73,7 +76,7 @@ export default function RecentChanges({ spaceId }: Props) { ) : ( - No pages yet + {t("No pages yet")} ); } diff --git a/apps/client/src/components/layouts/global/app-header.tsx b/apps/client/src/components/layouts/global/app-header.tsx index c83bf4f8..7016af55 100644 --- a/apps/client/src/components/layouts/global/app-header.tsx +++ b/apps/client/src/components/layouts/global/app-header.tsx @@ -11,10 +11,14 @@ import { } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts"; import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx"; +import { useTranslation } from "react-i18next"; const links = [{ link: APP_ROUTE.HOME, label: "Home" }]; export function AppHeader() { + const { t } = useTranslation("translation", { + keyPrefix: "layout", + }); const [mobileOpened] = useAtom(mobileSidebarAtom); const toggleMobile = useToggleSidebar(mobileSidebarAtom); @@ -25,7 +29,7 @@ export function AppHeader() { const items = links.map((link) => ( - {link.label} + {t(link.label)} )); diff --git a/apps/client/src/components/layouts/global/top-menu.tsx b/apps/client/src/components/layouts/global/top-menu.tsx index cd7527c0..d30e72e4 100644 --- a/apps/client/src/components/layouts/global/top-menu.tsx +++ b/apps/client/src/components/layouts/global/top-menu.tsx @@ -13,8 +13,12 @@ import { Link } from "react-router-dom"; import APP_ROUTE from "@/lib/app-route.ts"; import useAuth from "@/features/auth/hooks/use-auth.ts"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import { useTranslation } from "react-i18next"; export default function TopMenu() { + const { t } = useTranslation("translation", { + keyPrefix: "layout", + }); const [currentUser] = useAtom(currentUserAtom); const { logout } = useAuth(); @@ -44,14 +48,14 @@ export default function TopMenu() { - Workspace + {t("Workspace")} } > - Workspace settings + {t("Workspace settings")} } > - Manage members + {t("Manage members")} - Account + {t("Account")} } > - My profile + {t("My profile")} } > - My preferences + {t("My preferences")} }> - Logout + {t("Logout")} diff --git a/apps/client/src/components/settings/settings-sidebar.tsx b/apps/client/src/components/settings/settings-sidebar.tsx index e4ee2799..0e1f8438 100644 --- a/apps/client/src/components/settings/settings-sidebar.tsx +++ b/apps/client/src/components/settings/settings-sidebar.tsx @@ -11,6 +11,7 @@ import { } from "@tabler/icons-react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import classes from "./settings.module.css"; +import { useTranslation } from "react-i18next"; interface DataItem { label: string; @@ -51,6 +52,9 @@ const groupedData: DataGroup[] = [ ]; export default function SettingsSidebar() { + const { t } = useTranslation("translation", { + keyPrefix: "layout", + }); const location = useLocation(); const [active, setActive] = useState(location.pathname); const navigate = useNavigate(); @@ -62,7 +66,7 @@ export default function SettingsSidebar() { const menuItems = groupedData.map((group) => (
- {group.heading} + {t(group.heading)} {group.items.map((item) => ( - {item.label} + {t(item.label)} ))}
@@ -89,7 +93,7 @@ export default function SettingsSidebar() { > - Settings + {t("Settings")} {menuItems} diff --git a/apps/client/src/features/auth/components/invite-sign-up-form.tsx b/apps/client/src/features/auth/components/invite-sign-up-form.tsx index 81752f57..45cda915 100644 --- a/apps/client/src/features/auth/components/invite-sign-up-form.tsx +++ b/apps/client/src/features/auth/components/invite-sign-up-form.tsx @@ -17,6 +17,7 @@ import useAuth from "@/features/auth/hooks/use-auth"; import classes from "@/features/auth/components/auth.module.css"; import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts"; import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; +import { useTranslation } from "react-i18next"; const formSchema = z.object({ name: z.string().min(2), @@ -26,6 +27,7 @@ const formSchema = z.object({ type FormValues = z.infer; export function InviteSignUpForm() { + const { t } = useTranslation("invite-signup"); const params = useParams(); const [searchParams] = useSearchParams(); @@ -55,7 +57,7 @@ export function InviteSignUpForm() { } if (isError) { - return
invalid invitation link
; + return
{t("invalid invitation link")}
; } if (!invitation) { @@ -66,7 +68,7 @@ export function InviteSignUpForm() { - Join the workspace + {t("Join the workspace")} @@ -74,8 +76,8 @@ export function InviteSignUpForm() { @@ -83,7 +85,7 @@ export function InviteSignUpForm() { diff --git a/apps/client/src/features/auth/components/login-form.tsx b/apps/client/src/features/auth/components/login-form.tsx index 9433b1af..376141d9 100644 --- a/apps/client/src/features/auth/components/login-form.tsx +++ b/apps/client/src/features/auth/components/login-form.tsx @@ -13,6 +13,7 @@ import { } from "@mantine/core"; import classes from "./auth.module.css"; import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; +import { useTranslation } from "react-i18next"; const formSchema = z.object({ email: z @@ -25,6 +26,7 @@ const formSchema = z.object({ export function LoginForm() { const { signIn, isLoading } = useAuth(); useRedirectIfAuthenticated(); + const { t } = useTranslation("login"); const form = useForm({ validate: zodResolver(formSchema), @@ -42,28 +44,28 @@ export function LoginForm() { - Login + {t("Login")}
diff --git a/apps/client/src/features/auth/components/setup-workspace-form.tsx b/apps/client/src/features/auth/components/setup-workspace-form.tsx index bb62918c..43d844cf 100644 --- a/apps/client/src/features/auth/components/setup-workspace-form.tsx +++ b/apps/client/src/features/auth/components/setup-workspace-form.tsx @@ -13,6 +13,7 @@ import { import { ISetupWorkspace } from "@/features/auth/types/auth.types"; import useAuth from "@/features/auth/hooks/use-auth"; import classes from "@/features/auth/components/auth.module.css"; +import { useTranslation } from "react-i18next"; const formSchema = z.object({ workspaceName: z.string().min(2).max(60), @@ -28,6 +29,7 @@ export function SetupWorkspaceForm() { const { setupWorkspace, isLoading } = useAuth(); // useRedirectIfAuthenticated(); + const { t } = useTranslation("setup-workspace"); const form = useForm({ validate: zodResolver(formSchema), initialValues: { @@ -46,15 +48,15 @@ export function SetupWorkspaceForm() { - Create workspace + {t("Create workspace")}
diff --git a/apps/client/src/features/group/components/add-group-member-modal.tsx b/apps/client/src/features/group/components/add-group-member-modal.tsx index 8b05c595..f7389168 100644 --- a/apps/client/src/features/group/components/add-group-member-modal.tsx +++ b/apps/client/src/features/group/components/add-group-member-modal.tsx @@ -4,8 +4,12 @@ import React, { useState } from "react"; import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx"; import { useParams } from "react-router-dom"; import { useAddGroupMemberMutation } from "@/features/group/queries/group-query.ts"; +import { useTranslation } from "react-i18next"; export default function AddGroupMemberModal() { + const { t } = useTranslation("settings", { + keyPrefix: "workspace.group", + }); const { groupId } = useParams(); const [opened, { open, close }] = useDisclosure(false); const [userIds, setUserIds] = useState([]); @@ -27,19 +31,19 @@ export default function AddGroupMemberModal() { return ( <> - + - + diff --git a/apps/client/src/features/group/components/create-group-form.tsx b/apps/client/src/features/group/components/create-group-form.tsx index 27730642..ad702017 100644 --- a/apps/client/src/features/group/components/create-group-form.tsx +++ b/apps/client/src/features/group/components/create-group-form.tsx @@ -5,6 +5,7 @@ import { useForm, zodResolver } from "@mantine/form"; import * as z from "zod"; import { useNavigate } from "react-router-dom"; import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx"; +import { useTranslation } from "react-i18next"; const formSchema = z.object({ name: z.string().min(2).max(50), @@ -14,6 +15,9 @@ const formSchema = z.object({ type FormValues = z.infer; export function CreateGroupForm() { + const { t } = useTranslation("settings", { + keyPrefix: "workspace.group", + }); const createGroupMutation = useCreateGroupMutation(); const [userIds, setUserIds] = useState([]); const navigate = useNavigate(); @@ -52,16 +56,16 @@ export function CreateGroupForm() {