From b71b78c2feb992aedf0a40ff407419e76d597f91 Mon Sep 17 00:00:00 2001 From: ORyong Date: Sun, 5 Jul 2026 14:15:11 +0800 Subject: [PATCH] Improve Simplified Chinese localization --- src/locales/en.json | 52 +++++++- src/locales/zh.json | 142 ++++++++++++++------- src/main/File/Archive.ts | 15 ++- src/main/File/Dialog.ts | 29 +++-- src/main/I18n.ts | 41 ++++++ src/main/IpcMain.ts | 8 +- src/main/Menu.ts | 79 +++++++----- src/main/Notifications.test.ts | 7 + src/main/Notifications.ts | 21 ++- src/main/Tray.ts | 8 +- src/renderer/Dialog/DatePicker.tsx | 6 + src/renderer/Drawer/Attributes.tsx | 2 +- src/renderer/Drawer/Sorting.tsx | 2 +- src/renderer/Settings/LanguageSelector.tsx | 24 ++-- src/renderer/Settings/Settings.tsx | 5 +- 15 files changed, 320 insertions(+), 121 deletions(-) create mode 100644 src/main/I18n.ts diff --git a/src/locales/en.json b/src/locales/en.json index e13441af..6f343787 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -75,7 +75,6 @@ "add": "Add", "todoDialog.input.placeholder": "Click the help button to learn what to type here", "todoDialog.footer.update": "Update", - "cancel": "Cancel", "close": "Close", "todoDialog.snackbar.emptyInput": "Please enter something into the text field", "todoDialog.priorityPicker.label": "Priority", @@ -97,5 +96,54 @@ "prompt.archive.changeFile.headline": "Choose a File for Archiving", "prompt.archive.changeFile.text": "To archive completed todos, you need to choose an archive file first", "prompt.searchFilters.delete.headline": "Delete search filter?", - "prompt.searchFilters.delete.body": "This search filter will be permanently deleted:" + "prompt.searchFilters.delete.body": "This search filter will be permanently deleted:", + "menu.about": "About", + "menu.aboutSleek": "About sleek", + "menu.revealConfigurationFolder": "Reveal configuration folder", + "menu.file": "File", + "menu.view": "View", + "menu.toggleDrawer": "Toggle drawer", + "menu.toggleFileTabs": "Toggle file tabs", + "menu.toggleNavigation": "Toggle navigation", + "menu.toggleTheme": "Toggle theme", + "menu.toggleCompactMode": "Toggle compact mode", + "menu.todos": "Todos", + "menu.addNewTodo": "Add new todo", + "menu.find": "Find", + "menu.toggleCompleted": "Toggle completed", + "menu.closeWindow": "Close window", + "menu.openWindow": "Open window", + "menu.help": "Help", + "menu.changelog": "Changelog", + "menu.reportBugs": "Report bugs", + "menu.discussFeatures": "Discuss new or existing features", + "menu.contributing": "Contributing", + "menu.keyboardShortcuts": "Keyboard shortcuts", + "menu.privacyPolicy": "Privacy policy", + "menu.sponsoring": "Sponsoring", + "menu.developerTools": "Developer tools", + "menu.showSleek": "Show sleek", + "menu.quitSleek": "Quit sleek", + "notification.dueToday": "Due today", + "notification.dueTomorrow": "Due tomorrow", + "notification.dueInDays": "Due in {{count}} days", + "menu.edit": "Edit", + "menu.undo": "Undo", + "menu.redo": "Redo", + "menu.cut": "Cut", + "menu.paste": "Paste", + "menu.selectAll": "Select all", + "menu.hide": "Hide", + "menu.reload": "Reload", + "fileDialog.textFiles": "Text files", + "fileDialog.allFiles": "All files", + "archive.error.todoFileNotDefined": "Todo file is not defined", + "archive.error.archivingFileNotDefined": "Archiving file is not defined", + "archive.error.noCompletedTodosFound": "No completed todos found", + "archive.success": "Successfully archived", + "clipboard.copied": "Copied to clipboard: {{value}}", + "settings.days": " days", + "sorting.ascending": "Ascending order", + "sorting.descending": "Descending order", + "pomodoro.alt": "Pomodoro" } diff --git a/src/locales/zh.json b/src/locales/zh.json index 11f4b524..23af52f9 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -2,47 +2,47 @@ "copy": "复制", "delete": "删除", "cancel": "取消", - "openFile": "打开 文件", - "createFile": "创建 文件", - "fileTabs.changeLocation": "更改文件以进行归档", + "openFile": "打开文件", + "createFile": "创建文件", + "fileTabs.changeLocation": "更改归档文件", "fileTabs.revealTodoFile": "在文件管理器中显示待办文件", "fileTabs.revealArchivingFile": "在文件管理器中显示归档文件", - "fileTabs.removeFileHeadline": "从 sleek 中删除文件?", - "fileTabs.removeFileText": "它不会从您的硬盘驱动器中删除。", - "fileTabs.removeFileLabel": "删除", + "fileTabs.removeFileHeadline": "从 sleek 中移除文件?", + "fileTabs.removeFileText": "这不会从你的硬盘中删除该文件", + "fileTabs.removeFileLabel": "移除", "settings": "设置", - "settings.appendCreationDate": "创建任务时附加创建日期", + "settings.appendCreationDate": "创建待办时附加创建日期", "settings.convertRelativeToAbsoluteDates": "将相对日期转换为绝对日期", "settings.notificationsAllowed": "发送通知", - "settings.showFileTabs": "显示文件选项卡", - "settings.tray": "最小化到托盘而不是退出应用程序", + "settings.showFileTabs": "显示文件标签页", + "settings.tray": "最小化到托盘,而不是退出应用", "settings.invertTrayColor": "反转托盘图标颜色", - "settings.startMinimized": "以最小化状态启动应用程序", + "settings.startMinimized": "启动时最小化到托盘", "settings.colorTheme": "主题", "settings.system": "跟随系统", - "settings.light": "明亮", - "settings.dark": "暗黑", + "settings.light": "浅色", + "settings.dark": "深色", "settings.language": "语言", - "settings.notificationThreshold": "通知阈值", - "settings.bulkTodoCreation": "多行文本创建单独的待办事项", - "settings.matomo": "允许Matomo分析", + "settings.notificationThreshold": "通知提前天数", + "settings.bulkTodoCreation": "多行文本分别创建待办", + "settings.matomo": "允许 Matomo 分析", "settings.zoom": "缩放", "settings.disableAnimations": "禁用动画", - "settings.useHumanFriendlyDates": "使用人性化日期", - "settings.weekStart": "一周的第一天", + "settings.useHumanFriendlyDates": "使用易读日期", + "settings.weekStart": "每周开始于", "settings.0": "星期日", "settings.1": "星期一", - "settings.6": "周六", + "settings.6": "星期六", "settings.menuBarVisibility": "显示菜单栏", - "settings.compact": "精简视图", + "settings.compact": "紧凑视图", "attributes": "属性", - "filters": "过滤器", + "filters": "筛选", "sorting": "排序", - "drawer.filters.showCompleted": "已完成任务", - "drawer.filters.showHidden": "隐藏任务", + "drawer.filters.showCompleted": "已完成的待办", + "drawer.filters.showHidden": "隐藏的待办", "drawer.filters.thresholdDateInTheFuture": "未来的阈值日期", "drawer.filters.dueDateInTheFuture": "未来的截止日期", - "drawer.attributes.noAttributesAvailable": "目前无可用属性", + "drawer.attributes.noAttributesAvailable": "暂无可用属性", "drawer.attributes.today": "今天", "drawer.attributes.tomorrow": "明天", "drawer.attributes.nextWeek": "下周", @@ -51,33 +51,32 @@ "drawer.attributes.thisWeek": "本周", "drawer.attributes.thisMonth": "本月", "drawer.attributes.nextMonth": "下个月", - "drawer.attributes.overdue": "过期", - "drawer.attributes.elapsed": "已过", - "drawer.sorting.fileSorting": "按照它们在文件中出现的顺序排序任务", - "drawer.sorting.sortCompletedLast": "将已完成的任务移动到列表末尾", + "drawer.attributes.overdue": "已逾期", + "drawer.attributes.elapsed": "已开始", + "drawer.sorting.fileSorting": "按文件中的出现顺序排列待办", + "drawer.sorting.sortCompletedLast": "将已完成的待办移到列表末尾", "shared.attributeMapping.t": "阈值日期", "shared.attributeMapping.due": "截止日期", "shared.attributeMapping.projects": "项目", "shared.attributeMapping.contexts": "上下文", "shared.attributeMapping.priority": "优先级", "shared.attributeMapping.rec": "重复", - "shared.attributeMapping.pm": "番茄工作法", + "shared.attributeMapping.pm": "番茄钟", "shared.attributeMapping.created": "创建日期", "shared.attributeMapping.completed": "完成日期", - "splashscreen.noFiles.text": "打开一个现有的todo.txt文件或创建一个新的", - "splashscreen.noTodosAvailable.text": "此文件中没有任务", - "splashscreen.noTodosAvailable.create": "创建任务", - "splashscreen.noTodosVisible.text": "没有可见的结果。", - "splashscreen.noTodosVisible.reset": "重置过滤器", - "search.visibleTodos": "显示 {{visible}} 个(共 {{total}} 个)待办事项", - "search.addAsTodo": "添加为任务", - "search.filters.create": "创建搜索过滤器", + "splashscreen.noFiles.text": "打开现有 todo.txt 文件,或创建一个新文件", + "splashscreen.noTodosAvailable.text": "这个文件中没有待办", + "splashscreen.noTodosAvailable.create": "创建待办", + "splashscreen.noTodosVisible.text": "没有可见结果", + "splashscreen.noTodosVisible.reset": "重置筛选", + "search.visibleTodos": "显示 {{visible}} 个,共 {{total}} 个待办", + "search.addAsTodo": "添加为待办", + "search.filters.create": "创建搜索筛选", "add": "添加", - "todoDialog.input.placeholder": "点击帮助按钮以了解在此处输入什么", + "todoDialog.input.placeholder": "点击帮助按钮,了解这里可以输入什么", "todoDialog.footer.update": "更新", - "cancel": "取消", "close": "关闭", - "todoDialog.snackbar.emptyInput": "请在文本字段中输入内容", + "todoDialog.snackbar.emptyInput": "请在文本框中输入内容", "todoDialog.priorityPicker.label": "优先级", "todoDialog.datePicker.t": "阈值", "todoDialog.datePicker.due": "截止", @@ -89,13 +88,62 @@ "todoDialog.recurrencePicker.month": "月", "todoDialog.recurrencePicker.year": "年", "todoDialog.recurrencePicker.strict": "严格", - "prompt.delete.headline": "删除任务?", - "prompt.delete.text": "任务将永久从文件中删除", - "prompt.archive.headline": "归档已完成的任务?", - "prompt.archive.text": "这将把所有已完成的待办事项移动到您指定的归档文件中", + "prompt.delete.headline": "删除待办?", + "prompt.delete.text": "该待办将从文件中永久删除", + "prompt.archive.headline": "归档已完成的待办?", + "prompt.archive.text": "这会将所有已完成的待办移动到你指定的归档文件", "archive": "归档", - "prompt.archive.changeFile.headline": "选择一个文件进行归档", - "prompt.archive.changeFile.text": "要归档已完成的任务,您需要首先选择一个归档文件。", - "prompt.searchFilters.delete.headline": "删除搜索过滤器?", - "prompt.searchFilters.delete.body": "此搜索过滤器将被永久删除:" + "prompt.archive.changeFile.headline": "选择归档文件", + "prompt.archive.changeFile.text": "要归档已完成的待办,需要先选择一个归档文件", + "prompt.searchFilters.delete.headline": "删除搜索筛选?", + "prompt.searchFilters.delete.body": "此搜索筛选将被永久删除:", + "menu.about": "关于", + "menu.aboutSleek": "关于 sleek", + "menu.revealConfigurationFolder": "显示配置文件夹", + "menu.file": "文件", + "menu.view": "视图", + "menu.toggleDrawer": "显示/隐藏抽屉", + "menu.toggleFileTabs": "显示/隐藏文件标签页", + "menu.toggleNavigation": "显示/隐藏导航", + "menu.toggleTheme": "切换主题", + "menu.toggleCompactMode": "切换紧凑模式", + "menu.todos": "待办", + "menu.addNewTodo": "新建待办", + "menu.find": "查找", + "menu.toggleCompleted": "显示/隐藏已完成", + "menu.closeWindow": "关闭窗口", + "menu.openWindow": "打开窗口", + "menu.help": "帮助", + "menu.changelog": "更新日志", + "menu.reportBugs": "报告问题", + "menu.discussFeatures": "讨论新功能或现有功能", + "menu.contributing": "参与贡献", + "menu.keyboardShortcuts": "键盘快捷键", + "menu.privacyPolicy": "隐私政策", + "menu.sponsoring": "赞助", + "menu.developerTools": "开发者工具", + "menu.showSleek": "显示 sleek", + "menu.quitSleek": "退出 sleek", + "notification.dueToday": "今天截止", + "notification.dueTomorrow": "明天截止", + "notification.dueInDays": "{{count}} 天后截止", + "menu.edit": "编辑", + "menu.undo": "撤销", + "menu.redo": "重做", + "menu.cut": "剪切", + "menu.paste": "粘贴", + "menu.selectAll": "全选", + "menu.hide": "隐藏", + "menu.reload": "重新加载", + "fileDialog.textFiles": "文本文件", + "fileDialog.allFiles": "所有文件", + "archive.error.todoFileNotDefined": "未指定待办文件", + "archive.error.archivingFileNotDefined": "未指定归档文件", + "archive.error.noCompletedTodosFound": "没有找到已完成的待办", + "archive.success": "归档成功", + "clipboard.copied": "已复制到剪贴板:{{value}}", + "settings.days": " 天", + "sorting.ascending": "升序", + "sorting.descending": "降序", + "pomodoro.alt": "番茄钟" } diff --git a/src/main/File/Archive.ts b/src/main/File/Archive.ts index 82079315..9e45bbf9 100644 --- a/src/main/File/Archive.ts +++ b/src/main/File/Archive.ts @@ -2,14 +2,19 @@ import { getActiveFile } from "./Active"; import { readFileContent } from "./File"; import { writeToFile } from "./Write"; import { mainWindow } from "../index"; +import { SettingsStore } from "../Stores"; +import { TranslateMain } from "../I18n"; import { File } from "@sleek-types"; const COMPLETION_MARKER = "x "; +const t = (key: Parameters[0]) => + TranslateMain(key, SettingsStore.get("language")); + function checkArchiveReadiness(): void { const activeFile: File | null = getActiveFile(); if (!activeFile) { - throw new Error("Todo file is not defined"); + throw new Error(t("archive.error.todoFileNotDefined")); } mainWindow!.webContents.send( "triggerArchiving", @@ -31,11 +36,11 @@ function filterByCompletion(content: string, complete: boolean): string { function archiveTodos(): string { const activeFile = getActiveFile(); if (!activeFile) { - throw new Error("Todo file is not defined"); + throw new Error(t("archive.error.todoFileNotDefined")); } if (!activeFile.doneFilePath) { - throw new Error("Archiving file is not defined"); + throw new Error(t("archive.error.archivingFileNotDefined")); } // Read todo file only once @@ -49,7 +54,7 @@ function archiveTodos(): string { const uncompletedTodos = filterByCompletion(todoContent, false); if (!completedTodos.trim()) { - throw new Error("No completed todos found"); + throw new Error(t("archive.error.noCompletedTodosFound")); } const todosFromDoneFile: string = readFileContent( @@ -68,7 +73,7 @@ function archiveTodos(): string { writeToFile(contentForDoneFile, activeFile.doneFilePath); writeToFile(uncompletedTodos, activeFile.todoFilePath); - return "Successfully archived"; + return t("archive.success"); } export { archiveTodos, checkArchiveReadiness }; diff --git a/src/main/File/Dialog.ts b/src/main/File/Dialog.ts index 0bcdfe5b..44822bd9 100644 --- a/src/main/File/Dialog.ts +++ b/src/main/File/Dialog.ts @@ -7,22 +7,27 @@ import { import path from "path"; import { registerTodoFile, linkDoneFile } from "./File"; import { writeToFile } from "./Write"; +import { SettingsStore } from "../Stores"; +import { TranslateMain } from "../I18n"; -const dialogFilters = [ - { - name: "Text files", - extensions: ["txt"], - }, - { - name: "All files", - extensions: ["*"], - }, -]; +const dialogFilters = () => { + const language = SettingsStore.get("language"); + return [ + { + name: TranslateMain("fileDialog.textFiles", language), + extensions: ["txt"], + }, + { + name: TranslateMain("fileDialog.allFiles", language), + extensions: ["*"], + }, + ]; +}; async function openFile(setDoneFile: boolean): Promise { const result: OpenDialogReturnValue = await dialog.showOpenDialog({ properties: ["openFile"], - filters: dialogFilters, + filters: dialogFilters(), securityScopedBookmarks: true, }); if (!result.canceled && result.filePaths.length > 0) { @@ -41,7 +46,7 @@ async function createFile(setDoneFile: boolean): Promise { const defaultFileName = setDoneFile ? "done.txt" : "todo.txt"; const result: SaveDialogReturnValue = await dialog.showSaveDialog({ defaultPath: path.join(app.getPath("documents"), defaultFileName), - filters: dialogFilters, + filters: dialogFilters(), securityScopedBookmarks: true, }); diff --git a/src/main/I18n.ts b/src/main/I18n.ts new file mode 100644 index 00000000..7ad9770a --- /dev/null +++ b/src/main/I18n.ts @@ -0,0 +1,41 @@ +import { app } from "electron"; +import en from "../locales/en.json"; +import zh from "../locales/zh.json"; + +type TranslationKey = keyof typeof en; +type Replacements = Record; + +const resources = { + en, + zh, +}; + +export function GetMainLanguage( + configuredLanguage?: string, +): keyof typeof resources { + const appLocale = + typeof app.getLocale === "function" ? app.getLocale() : "en"; + const language = (configuredLanguage || appLocale || "en") + .toLowerCase() + .slice(0, 2); + + return language === "zh" ? "zh" : "en"; +} + +export function TranslateMain( + key: TranslationKey, + configuredLanguage?: string, + replacements: Replacements = {}, +): string { + const language = GetMainLanguage(configuredLanguage); + let value = resources[language][key] || resources.en[key] || key; + + Object.entries(replacements).forEach(([replacementKey, replacementValue]) => { + value = value.replaceAll( + `{{${replacementKey}}}`, + replacementValue.toString(), + ); + }); + + return value; +} diff --git a/src/main/IpcMain.ts b/src/main/IpcMain.ts index c0c04037..26edea19 100644 --- a/src/main/IpcMain.ts +++ b/src/main/IpcMain.ts @@ -8,6 +8,7 @@ import { HandleError } from "./Shared"; import { registerTodoFile, activateFile, removeFile } from "./File/File"; import { openFile, createFile } from "./File/Dialog"; import { createTodoObject } from "./DataRequest/CreateTodoObjects"; +import { TranslateMain } from "./I18n"; function handleDataRequest(event: IpcMainEvent, searchString: string) { try { @@ -204,7 +205,12 @@ function handleArchiveTodos(event: IpcMainEvent): void { function handleSaveToClipboard(event: IpcMainEvent, string: string): void { try { clipboard.writeText(string); - event.reply("responseFromMainProcess", "Copied to clipboard: " + string); + event.reply( + "responseFromMainProcess", + TranslateMain("clipboard.copied", SettingsStore.get("language"), { + value: string, + }), + ); } catch (error) { if (error instanceof Error) HandleError(error); } diff --git a/src/main/Menu.ts b/src/main/Menu.ts index 14eab4b3..570e4ca8 100644 --- a/src/main/Menu.ts +++ b/src/main/Menu.ts @@ -6,6 +6,7 @@ import { checkArchiveReadiness } from "./File/Archive"; import { SettingsStore, FiltersStore } from "./Stores"; import { File } from "../@types"; import appPackage from "../../package.json"; +import { TranslateMain } from "./I18n"; export const GetFileMenuEntries = (files: File[]) => { return files.map((file: File, index: number) => ({ @@ -20,19 +21,23 @@ export const GetFileMenuEntries = (files: File[]) => { const GetMenuTemplate = ( files: File[], ): Electron.MenuItemConstructorOptions[] => { + const language = SettingsStore.get("language"); + const t = (key: Parameters[0]) => + TranslateMain(key, language); + return [ { label: "sleek", type: "submenu", submenu: [ { - label: "About", + label: t("menu.about"), click: async () => { const options = { - title: "About sleek", + title: t("menu.aboutSleek"), message: `sleek v${app.getVersion()}`, detail: appPackage.description, - buttons: ["Close", "Reveal configuration folder"], + buttons: [t("close"), t("menu.revealConfigurationFolder")], }; const { response } = await dialog.showMessageBox(options); if (response === 1) { @@ -42,7 +47,7 @@ const GetMenuTemplate = ( }, }, { - label: "Settings", + label: t("settings"), accelerator: "CmdOrCtrl+,", click: () => { mainWindow!.webContents.send("isSettingsOpen", true); @@ -52,6 +57,7 @@ const GetMenuTemplate = ( ? ([ { type: "separator" }, { + label: t("menu.hide"), accelerator: "Cmd+H", role: "hide", }, @@ -59,6 +65,7 @@ const GetMenuTemplate = ( : []), { type: "separator" }, { + label: t("menu.quitSleek"), role: "quit", accelerator: "CmdOrCtrl+Q", }, @@ -66,18 +73,18 @@ const GetMenuTemplate = ( }, { id: "fileMenu", - label: "File", + label: t("menu.file"), submenu: [ { type: "separator" }, { - label: "Open file", + label: t("openFile"), accelerator: "CmdOrCtrl+O", click: async () => { await openFile(false); }, }, { - label: "Create file", + label: t("createFile"), click: async () => { await createFile(false); }, @@ -87,15 +94,24 @@ const GetMenuTemplate = ( ], }, { - role: "editMenu", + label: t("menu.edit"), + submenu: [ + { label: t("menu.undo"), role: "undo" }, + { label: t("menu.redo"), role: "redo" }, + { type: "separator" }, + { label: t("menu.cut"), role: "cut" }, + { label: t("copy"), role: "copy" }, + { label: t("menu.paste"), role: "paste" }, + { label: t("menu.selectAll"), role: "selectAll" }, + ], }, { - label: "View", + label: t("menu.view"), submenu: [ ...(files?.length > 0 ? [ { - label: "Toggle drawer", + label: t("menu.toggleDrawer"), accelerator: "CmdOrCtrl+B", click: () => { const isDrawerOpen = SettingsStore.get("isDrawerOpen"); @@ -103,7 +119,7 @@ const GetMenuTemplate = ( }, }, { - label: "Toggle file tabs", + label: t("menu.toggleFileTabs"), click: () => { const showFileTabs = SettingsStore.get("showFileTabs"); SettingsStore.set("showFileTabs", !showFileTabs); @@ -112,7 +128,7 @@ const GetMenuTemplate = ( ] : []), { - label: "Toggle navigation", + label: t("menu.toggleNavigation"), accelerator: "Ctrl+Alt+H", click: () => { const isNavigationOpen = SettingsStore.get("isNavigationOpen"); @@ -120,7 +136,7 @@ const GetMenuTemplate = ( }, }, { - label: "Toggle theme", + label: t("menu.toggleTheme"), accelerator: "Ctrl+Alt+D", click: () => { const shouldUseDarkColors = SettingsStore.get( @@ -133,7 +149,7 @@ const GetMenuTemplate = ( }, }, { - label: "Toggle compact mode", + label: t("menu.toggleCompactMode"), click: () => { const compact = SettingsStore.get("compact"); SettingsStore.set("compact", !compact); @@ -144,17 +160,17 @@ const GetMenuTemplate = ( ...(files?.length > 0 ? [ { - label: "Todos", + label: t("menu.todos"), submenu: [ { - label: "Add new todo", + label: t("menu.addNewTodo"), accelerator: "CmdOrCtrl+N", click: () => { mainWindow?.webContents.send("isDialogOpen"); }, }, { - label: "Find", + label: t("menu.find"), accelerator: "CmdOrCtrl+F", click: () => { const isSearchOpen = SettingsStore.get("isSearchOpen"); @@ -162,7 +178,7 @@ const GetMenuTemplate = ( }, }, { - label: "Toggle completed", + label: t("menu.toggleCompleted"), accelerator: "Ctrl+H", click: async () => { const showCompleted = SettingsStore.get("showCompleted"); @@ -170,14 +186,14 @@ const GetMenuTemplate = ( }, }, { - label: "Reset filters", + label: t("splashscreen.noTodosVisible.reset"), accelerator: "CmdOrCtrl+0", click: async () => { FiltersStore.set("attributes", {}); }, }, { - label: "Archive completed todos", + label: t("prompt.archive.headline"), accelerator: "Ctrl+Alt+A", click: () => { checkArchiveReadiness(); @@ -191,12 +207,12 @@ const GetMenuTemplate = ( role: "window", submenu: [ { - label: "Close window", + label: t("menu.closeWindow"), accelerator: "CmdOrCtrl+W", role: "close", }, { - label: "Open window", + label: t("menu.openWindow"), click: () => { HandleCreateWindow(); }, @@ -204,7 +220,7 @@ const GetMenuTemplate = ( ], }, { - label: "Help", + label: t("menu.help"), submenu: [ { label: "sleek wiki", @@ -213,7 +229,7 @@ const GetMenuTemplate = ( }, }, { - label: "Changelog", + label: t("menu.changelog"), click: () => { shell?.openExternal( "https://github.com/ransome1/sleek/blob/main/CHANGELOG.md", @@ -221,13 +237,13 @@ const GetMenuTemplate = ( }, }, { - label: "Report bugs", + label: t("menu.reportBugs"), click: () => { shell?.openExternal("https://github.com/ransome1/sleek/issues"); }, }, { - label: "Discuss new or existing features", + label: t("menu.discussFeatures"), click: () => { shell?.openExternal( "https://github.com/ransome1/sleek/discussions", @@ -235,7 +251,7 @@ const GetMenuTemplate = ( }, }, { - label: "Contributing", + label: t("menu.contributing"), click: () => { shell?.openExternal( "https://github.com/ransome1/sleek/blob/master/CONTRIBUTING.md", @@ -243,7 +259,7 @@ const GetMenuTemplate = ( }, }, { - label: "Keyboard shortcuts", + label: t("menu.keyboardShortcuts"), click: () => { shell?.openExternal( "https://github.com/ransome1/sleek/wiki/Keyboard-shortcuts#v2x", @@ -251,7 +267,7 @@ const GetMenuTemplate = ( }, }, { - label: "Privacy policy", + label: t("menu.privacyPolicy"), click: () => { shell?.openExternal( "https://github.com/ransome1/sleek/blob/master/PRIVACY.md", @@ -261,7 +277,7 @@ const GetMenuTemplate = ( ...(!process.mas ? [ { - label: "Sponsoring", + label: t("menu.sponsoring"), click: () => { shell?.openExternal("https://github.com/sponsors/ransome1"); }, @@ -270,10 +286,11 @@ const GetMenuTemplate = ( : []), { role: "toggleDevTools", - label: "Developer tools", + label: t("menu.developerTools"), }, { role: "reload", + label: t("menu.reload"), }, ], }, diff --git a/src/main/Notifications.test.ts b/src/main/Notifications.test.ts index e32cebd2..f60efa34 100644 --- a/src/main/Notifications.test.ts +++ b/src/main/Notifications.test.ts @@ -83,6 +83,7 @@ vi.mock("electron", () => { }; }), app: { + getLocale: vi.fn().mockReturnValue("en"), getPath: vi.fn().mockReturnValue(""), }, }; @@ -133,6 +134,12 @@ describe("CreateTitle", () => { it("Empty string if due date is in the past", () => { expect(CreateTitle(lastWeek, today)).toBe(""); }); + + it("uses Chinese titles when language is zh", () => { + expect(CreateTitle(today, today, "zh")).toBe("今天截止"); + expect(CreateTitle(tomorrow, today, "zh")).toBe("明天截止"); + expect(CreateTitle(inFourDays, today, "zh")).toBe("4 天后截止"); + }); }); describe("SuppressNotification", () => { diff --git a/src/main/Notifications.ts b/src/main/Notifications.ts index aee21dde..4d2284d2 100644 --- a/src/main/Notifications.ts +++ b/src/main/Notifications.ts @@ -4,6 +4,7 @@ import { Notification } from "electron"; import { SettingsStore, NotificationsStore, FiltersStore } from "./Stores"; import { checkForSearchMatches } from "./Filters/Search"; import { SearchFilter, Badge } from "@sleek-types"; +import { TranslateMain } from "./I18n"; // ─── Store accessors ──────────────────────────────────────────────────────── @@ -25,13 +26,23 @@ export function MustNotify(dueDate: DateTime, thresholdDay: DateTime): boolean { return dueDate < thresholdDay; } -export function CreateTitle(dueDate: DateTime, today: DateTime): string { +export function CreateTitle( + dueDate: DateTime, + today: DateTime, + language?: string, +): string { const tomorrow: DateTime = today.plus({ days: 1 }); const daysUntilDue: number = dueDate.diff(today, "days").toObject().days ?? 0; - if (dueDate.hasSame(today, "day")) return "Due today"; - if (dueDate.hasSame(tomorrow, "day")) return "Due tomorrow"; - return daysUntilDue > 1 ? `Due in ${daysUntilDue} days` : ""; + if (dueDate.hasSame(today, "day")) + return TranslateMain("notification.dueToday", language); + if (dueDate.hasSame(tomorrow, "day")) + return TranslateMain("notification.dueTomorrow", language); + return daysUntilDue > 1 + ? TranslateMain("notification.dueInDays", language, { + count: daysUntilDue, + }) + : ""; } export function SuppressNotification( @@ -77,7 +88,7 @@ export function HandleNotification( if (SuppressNotification(dueDate, body, hash, today, thresholdDay)) return; const notification = new Notification({ - title: CreateTitle(dueDate, today), + title: CreateTitle(dueDate, today, SettingsStore.get("language")), body, silent: false, }); diff --git a/src/main/Tray.ts b/src/main/Tray.ts index a72cb993..f8506456 100644 --- a/src/main/Tray.ts +++ b/src/main/Tray.ts @@ -3,6 +3,7 @@ import { HandleCreateWindow, mainWindow } from "./index"; import { GetFileMenuEntries } from "./Menu"; import { SettingsStore } from "./Stores"; import { File } from "../@types"; +import { TranslateMain } from "./I18n"; import TrayIconDark from "../../resources/trayDarkTemplate.png?asset"; import TrayIconLight from "../../resources/trayLightTemplate.png?asset"; import TrayIconDarkWin from "../../resources/trayDark.ico?asset"; @@ -55,12 +56,15 @@ export function UpdateTrayImage(): void { export function UpdateTrayMenu(): void { if (tray === null) return; const files: File[] = SettingsStore.get("files"); + const language = SettingsStore.get("language"); + const t = (key: Parameters[0]) => + TranslateMain(key, language); const menu: Electron.Menu = Menu.buildFromTemplate([ - { label: "Show sleek", click: HandleCreateWindow }, + { label: t("menu.showSleek"), click: HandleCreateWindow }, { type: "separator" }, ...GetFileMenuEntries(files), { type: "separator" }, - { label: "Quit sleek", click: app.quit }, + { label: t("menu.quitSleek"), click: app.quit }, ]); tray.setContextMenu(menu); } diff --git a/src/renderer/Dialog/DatePicker.tsx b/src/renderer/Dialog/DatePicker.tsx index 0a41c548..b39c8b1e 100644 --- a/src/renderer/Dialog/DatePicker.tsx +++ b/src/renderer/Dialog/DatePicker.tsx @@ -1,6 +1,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; +import { zhCN } from "@mui/x-date-pickers/locales"; import { AdapterLuxon } from "@mui/x-date-pickers/AdapterLuxon"; import { DateTime, Settings as LuxonSettings, WeekdayNumbers } from "luxon"; import "./DatePicker.scss"; @@ -28,11 +29,16 @@ const DatePickerComponent: React.FC = ({ }; const { t } = useTranslation(); + const localeText = + settings.language === "zh" + ? zhCN.components.MuiLocalizationProvider.defaultProps.localeText + : undefined; return ( = className={categoryKey === "pm" ? "pomodoro" : undefined} > {categoryKey === "pm" && ( - Pomodoro + {t("pomodoro.alt")} )} {bucketName} diff --git a/src/renderer/Drawer/Sorting.tsx b/src/renderer/Drawer/Sorting.tsx index 196994eb..070425be 100644 --- a/src/renderer/Drawer/Sorting.tsx +++ b/src/renderer/Drawer/Sorting.tsx @@ -120,7 +120,7 @@ const DrawerSortingComponent: React.FC = ({ onClick={() => toggleInvert(index)} data-testid={`drawer-sorting-draggable-list-item-${item.value}-invert`} aria-label={ - item.invert ? "Descending order" : "Ascending order" + item.invert ? t("sorting.descending") : t("sorting.ascending") } > {!item.invert ? : } diff --git a/src/renderer/Settings/LanguageSelector.tsx b/src/renderer/Settings/LanguageSelector.tsx index 2b20fa72..fc5aaae5 100644 --- a/src/renderer/Settings/LanguageSelector.tsx +++ b/src/renderer/Settings/LanguageSelector.tsx @@ -88,19 +88,19 @@ const friendlyLanguageName: Record = { de: "Deutsch", en: "English", it: "Italiano", - es: "Español", - fr: "Français", - zh: "简体中文", - pt: "Português (Portugal)", - "pt-br": "Português (Brasil)", - jp: "日本語", - tr: "Türkçe", + es: "Espa\u00f1ol", + fr: "Fran\u00e7ais", + zh: "\u7b80\u4f53\u4e2d\u6587", + pt: "Portugu\u00eas (Portugal)", + "pt-br": "Portugu\u00eas (Brasil)", + jp: "\u65e5\u672c\u8a9e", + tr: "T\u00fcrk\u00e7e", hu: "Magyar", - cs: "Čeština", + cs: "\u010ce\u0161tina", pl: "Polski", - ru: "Русский", - ko: "한국어", - hi: "हिन्दी", + ru: "\u0420\u0443\u0441\u0441\u043a\u0438\u0439", + ko: "\ud55c\uad6d\uc5b4", + hi: "\u0939\u093f\u0928\u094d\u0926\u0940", }; interface LanguageSelectorComponentProps { @@ -119,7 +119,7 @@ const LanguageSelectorComponent: React.FC = ({