diff --git a/configs/i18n-completeness.ts b/configs/i18n-completeness.ts new file mode 100644 index 000000000..3de328b31 --- /dev/null +++ b/configs/i18n-completeness.ts @@ -0,0 +1,60 @@ +import fs from "node:fs" +import path from "node:path" + +type LanguageCompletion = Record + +function getLanguageFiles(dir: string): string[] { + return fs.readdirSync(dir).filter((file) => file.endsWith(".json")) +} + +function getNamespaces(localesDir: string): string[] { + return fs + .readdirSync(localesDir) + .filter((file) => fs.statSync(path.join(localesDir, file)).isDirectory()) +} + +function countKeys(obj: any): number { + let count = 0 + for (const key in obj) { + if (typeof obj[key] === "object") { + count += countKeys(obj[key]) + } else { + count++ + } + } + return count +} + +function calculateCompleteness(localesDir: string): LanguageCompletion { + const namespaces = getNamespaces(localesDir) + const languages = new Set() + const keyCount: Record = {} + + namespaces.forEach((namespace) => { + const namespaceDir = path.join(localesDir, namespace) + const files = getLanguageFiles(namespaceDir) + + files.forEach((file) => { + const lang = path.basename(file, ".json") + languages.add(lang) + + const content = JSON.parse(fs.readFileSync(path.join(namespaceDir, file), "utf-8")) + keyCount[lang] = (keyCount[lang] || 0) + countKeys(content) + }) + }) + + const enCount = keyCount["en"] || 0 + const completeness: LanguageCompletion = {} + + languages.forEach((lang) => { + if (lang !== "en") { + const percent = Math.round((keyCount[lang] / enCount) * 100) + completeness[lang] = percent + } + }) + + return completeness +} + +const i18n = calculateCompleteness(path.resolve(__dirname, "../locales")) +export default i18n diff --git a/configs/vite.render.config.ts b/configs/vite.render.config.ts index 36dd7d4f3..8dfb9747b 100644 --- a/configs/vite.render.config.ts +++ b/configs/vite.render.config.ts @@ -11,6 +11,7 @@ import { prerelease } from "semver" import type { Plugin, UserConfig } from "vite" import { getGitHash } from "../scripts/lib" +import i18nCompleteness from "./i18n-completeness" const pkg = JSON.parse(readFileSync("package.json", "utf8")) const isCI = process.env.CI === "true" || process.env.CI === "1" @@ -134,6 +135,8 @@ export const viteRenderBaseConfig = { RELEASE_CHANNEL: JSON.stringify((prerelease(pkg.version)?.[0] as string) || "stable"), DEBUG: process.env.DEBUG === "true", + + I18N_COMPLETENESS_MAP: JSON.stringify({ ...i18nCompleteness, en: 100 }), }, } satisfies UserConfig diff --git a/locales/app/en.json b/locales/app/en.json index 18249e9ef..fe3cc71a9 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -2,6 +2,9 @@ "ai_daily.title": "Top News - {{title}}", "ai_daily.tooltip.content": "Here is news selected by AI from your timeline ( - ) that may be important to you.", "ai_daily.tooltip.update_schedule": "Update daily at 8 AM and 8 PM.", + "app.app_name": "APP_NAME", + "app.copy_logo_svg": "Copy Logo SVG", + "app.toggle_sidebar": "Toggle Sidebar", "discover.any_url_or_keyword": "Any URL or Keyword", "discover.default_option": " (default)", "discover.feed_description": "The description of this feed is as follows, and you can fill out the parameter form with the relevant information.", diff --git a/locales/app/zh-CN.json b/locales/app/zh-CN.json index 8e9f43af6..60bbaab6e 100644 --- a/locales/app/zh-CN.json +++ b/locales/app/zh-CN.json @@ -2,6 +2,9 @@ "ai_daily.title": "头条 - {{title}}", "ai_daily.tooltip.content": "这里是通过 AI 从您的时间线中选择的头条新闻( - ),可能对您很重要。", "ai_daily.tooltip.update_schedule": "每天早上 8 点、晚上 8 点更新。", + "app.app_name": "APP_NAME", + "app.copy_logo_svg": "复制 Logo SVG", + "app.toggle_sidebar": "切换侧边栏", "discover.any_url_or_keyword": "任何 URL 或关键词", "discover.default_option": " (默认)", "discover.feed_description": "此 Feed 的描述如下,您可以填写相关信息。", diff --git a/locales/common/en.json b/locales/common/en.json index 5350e008e..70164b71b 100644 --- a/locales/common/en.json +++ b/locales/common/en.json @@ -1,4 +1,5 @@ { + "app.copied_to_clipboard": "Copied to clipboard", "cancel": "Cancel", "confirm": "Confirm", "ok": "OK", diff --git a/locales/common/ja.json b/locales/common/ja.json new file mode 100644 index 000000000..14fb7935e --- /dev/null +++ b/locales/common/ja.json @@ -0,0 +1,28 @@ +{ + "app.copied_to_clipboard": "クリップボードにコピーしました", + "cancel": "キャンセル", + "confirm": "確認", + "ok": "OK", + "quantifier.piece": "個", + "time.last_night": "昨夜", + "time.the_night_before_last": "一昨夜", + "time.today": "今日", + "time.yesterday": "昨日", + "tips.load-lng-error": "言語パックの読み込みに失敗しました", + "words.back": "戻る", + "words.copy": "コピー", + "words.edit": "編集", + "words.entry": "エントリー", + "words.id": "ID", + "words.items_one": "アイテム", + "words.items_other": "アイテム", + "words.local": "ローカル", + "words.record": "記録", + "words.record_one": "記録", + "words.record_other": "記録", + "words.result": "結果", + "words.result_one": "結果", + "words.result_other": "結果", + "words.space": "", + "words.which.all": "すべて" +} diff --git a/locales/common/zh-CN.json b/locales/common/zh-CN.json index 9d94f3309..d68419a69 100644 --- a/locales/common/zh-CN.json +++ b/locales/common/zh-CN.json @@ -1,4 +1,5 @@ { + "app.copied_to_clipboard": "已复制到剪贴板", "cancel": "取消", "confirm": "确认", "ok": "好", diff --git a/locales/lang/en.json b/locales/lang/en.json index e6f2d29f5..795047911 100644 --- a/locales/lang/en.json +++ b/locales/lang/en.json @@ -1,5 +1,6 @@ { "langs.en": "English", - "langs.zh-CN": "简体中文(部分完成)", + "langs.ja": "日本語", + "langs.zh-CN": "简体中文", "name": "English" } diff --git a/locales/lang/ja.json b/locales/lang/ja.json new file mode 100644 index 000000000..cdd1c6e6c --- /dev/null +++ b/locales/lang/ja.json @@ -0,0 +1,6 @@ +{ + "langs.en": "English", + "langs.ja": "日本語", + "langs.zh-CN": "简体中文", + "name": "日本語" +} diff --git a/locales/lang/zh-CN.json b/locales/lang/zh-CN.json index e6f2d29f5..48ce191a7 100644 --- a/locales/lang/zh-CN.json +++ b/locales/lang/zh-CN.json @@ -1,5 +1,6 @@ { "langs.en": "English", - "langs.zh-CN": "简体中文(部分完成)", - "name": "English" + "langs.ja": "日本語", + "langs.zh-CN": "简体中文", + "name": "简体中文" } diff --git a/src/renderer/src/@types/constants.ts b/src/renderer/src/@types/constants.ts index f6bac9017..94c2bebd3 100644 --- a/src/renderer/src/@types/constants.ts +++ b/src/renderer/src/@types/constants.ts @@ -1,16 +1,9 @@ -export const currentSupportedLanguages = (() => { - const langsFiles = import.meta.glob("../../../../locales/app/*.json") - - const langs = [] as string[] - for (const key in langsFiles) { - langs.push(key.split("/").pop()?.replace(".json", "") as string) - } - return langs -})() +export const currentSupportedLanguages = ["en", "ja", "zh-CN"] export const dayjsLocaleImportMap = { en: ["en", () => import("dayjs/locale/en")], ["zh-CN"]: ["zh-cn", () => import("dayjs/locale/zh-cn")], + ["ja"]: ["ja", () => import("dayjs/locale/ja")], } export const ns = ["app", "common", "lang", "settings", "shortcuts"] as const diff --git a/src/renderer/src/@types/default-resource.ts b/src/renderer/src/@types/default-resource.ts index 51ddc8165..28c271ac8 100644 --- a/src/renderer/src/@types/default-resource.ts +++ b/src/renderer/src/@types/default-resource.ts @@ -1,8 +1,10 @@ import en from "../../../../locales/app/en.json" import common_en from "../../../../locales/common/en.json" +import common_ja from "../../../../locales/common/ja.json" import common_zhCN from "../../../../locales/common/zh-CN.json" import external_en from "../../../../locales/external/en.json" import lang_en from "../../../../locales/lang/en.json" +import lang_ja from "../../../../locales/lang/ja.json" import lang_zhCN from "../../../../locales/lang/zh-CN.json" import settings_en from "../../../../locales/settings/en.json" import shortcuts_en from "../../../../locales/shortcuts/en.json" @@ -28,4 +30,8 @@ export const defaultResources = { lang: lang_zhCN, common: common_zhCN, }, + ja: { + lang: lang_ja, + common: common_ja, + }, } diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index 6db251e83..06142aad0 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -2,3 +2,4 @@ declare const APP_VERSION: string declare const APP_NAME: string declare const RELEASE_CHANNEL: string +declare const I18N_COMPLETENESS_MAP: Record diff --git a/src/renderer/src/modules/entry-column/components/mark-all-button.tsx b/src/renderer/src/modules/entry-column/components/mark-all-button.tsx index dbc0690ff..1917030b2 100644 --- a/src/renderer/src/modules/entry-column/components/mark-all-button.tsx +++ b/src/renderer/src/modules/entry-column/components/mark-all-button.tsx @@ -1,7 +1,5 @@ -import { PopoverPortal } from "@radix-ui/react-popover" import { ActionButton, Button, IconButton } from "@renderer/components/ui/button" import { Kbd, KbdCombined } from "@renderer/components/ui/kbd/Kbd" -import { Popover, PopoverContent, PopoverTrigger } from "@renderer/components/ui/popover" import { RootPortal } from "@renderer/components/ui/portal" import { HotKeyScopeMap } from "@renderer/constants" import { shortcuts } from "@renderer/constants/shortcuts" @@ -176,65 +174,6 @@ const ConfirmMarkAllReadInfo = ({ undo }: { undo: () => any }) => { ) } -/** - * @deprecated - */ -export const MarkAllReadPopover = forwardRef( - ({ filter, className, which = "all", shortcut }, ref) => { - const [markPopoverOpen, setMarkPopoverOpen] = useState(false) - - const handleMarkAllAsRead = useMarkAllByRoute(filter) - - return ( - - - - Mark - - {which} - - as read - - } - className={className} - ref={ref} - > - - - - - -
- -
-
- } - onClick={() => { - handleMarkAllAsRead() - setMarkPopoverOpen(false) - }} - > - Confirm - -
-
-
-
- ) - }, -) - export const FlatMarkAllReadButton: FC = (props) => { const t = useI18n() diff --git a/src/renderer/src/modules/feed-column/header.tsx b/src/renderer/src/modules/feed-column/header.tsx index dcdbd90a1..ff75cc2e1 100644 --- a/src/renderer/src/modules/feed-column/header.tsx +++ b/src/renderer/src/modules/feed-column/header.tsx @@ -6,6 +6,7 @@ import { ActionButton } from "@renderer/components/ui/button" import { Popover, PopoverContent, PopoverTrigger } from "@renderer/components/ui/popover" import { ProfileButton } from "@renderer/components/user-button" import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" +import { useI18n } from "@renderer/hooks/common" import { stopPropagation } from "@renderer/lib/dom" import { cn } from "@renderer/lib/utils" import { m } from "framer-motion" @@ -53,7 +54,6 @@ export const FeedColumnHeader = memo(() => { }} > - {APP_NAME} @@ -80,10 +80,11 @@ const LayoutActionButton = () => { width: !feedColumnShow ? "auto" : 0, }) + const t = useI18n() return ( { const LogoContextMenu: FC = ({ children }) => { const [open, setOpen] = useState(false) const logoRef = useRef(null) + const t = useI18n() return ( @@ -120,7 +122,7 @@ const LogoContextMenu: FC = ({ children }) => { onClick={() => { navigator.clipboard.writeText(logoRef.current?.outerHTML || "") setOpen(false) - toast.success("Copied to clipboard") + toast.success(t.common("app.copied_to_clipboard")) }} className={cn( "relative flex cursor-default select-none items-center rounded-sm px-1 py-0.5 text-sm outline-none", @@ -129,7 +131,7 @@ const LogoContextMenu: FC = ({ children }) => { )} > - Copy Logo SVG + {t("app.copy_logo_svg")} diff --git a/src/renderer/src/modules/settings/tabs/general.tsx b/src/renderer/src/modules/settings/tabs/general.tsx index 0025fa0e3..de7ed2e76 100644 --- a/src/renderer/src/modules/settings/tabs/general.tsx +++ b/src/renderer/src/modules/settings/tabs/general.tsx @@ -221,11 +221,16 @@ export const LanguageSelector = () => { - {currentSupportedLanguages.map((lang) => ( - - {langT(`langs.${lang}` as any)} - - ))} + {currentSupportedLanguages.map((lang) => { + const percent = I18N_COMPLETENESS_MAP[lang] + + return ( + + {langT(`langs.${lang}` as any)}{" "} + {typeof percent === "number" ? (percent === 100 ? null : `(${percent}%)`) : null} + + ) + })} diff --git a/tsconfig.node.json b/tsconfig.node.json index 4203d84c0..35ccca4e3 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -9,7 +9,7 @@ "src/env.ts", "./src/hono.ts", "src/shared/src/global.d.ts", - "configs/vite.render.config.ts", + "configs/*", "./scripts/**/*" ], "compilerOptions": {