Skip to content

Commit bf7da04

Browse files
committed
feat: language indicator
Signed-off-by: Innei <i@innei.in>
1 parent d81c2b5 commit bf7da04

File tree

17 files changed

+136
-83
lines changed

17 files changed

+136
-83
lines changed

configs/i18n-completeness.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import fs from "node:fs"
2+
import path from "node:path"
3+
4+
type LanguageCompletion = Record<string, number>
5+
6+
function getLanguageFiles(dir: string): string[] {
7+
return fs.readdirSync(dir).filter((file) => file.endsWith(".json"))
8+
}
9+
10+
function getNamespaces(localesDir: string): string[] {
11+
return fs
12+
.readdirSync(localesDir)
13+
.filter((file) => fs.statSync(path.join(localesDir, file)).isDirectory())
14+
}
15+
16+
function countKeys(obj: any): number {
17+
let count = 0
18+
for (const key in obj) {
19+
if (typeof obj[key] === "object") {
20+
count += countKeys(obj[key])
21+
} else {
22+
count++
23+
}
24+
}
25+
return count
26+
}
27+
28+
function calculateCompleteness(localesDir: string): LanguageCompletion {
29+
const namespaces = getNamespaces(localesDir)
30+
const languages = new Set<string>()
31+
const keyCount: Record<string, number> = {}
32+
33+
namespaces.forEach((namespace) => {
34+
const namespaceDir = path.join(localesDir, namespace)
35+
const files = getLanguageFiles(namespaceDir)
36+
37+
files.forEach((file) => {
38+
const lang = path.basename(file, ".json")
39+
languages.add(lang)
40+
41+
const content = JSON.parse(fs.readFileSync(path.join(namespaceDir, file), "utf-8"))
42+
keyCount[lang] = (keyCount[lang] || 0) + countKeys(content)
43+
})
44+
})
45+
46+
const enCount = keyCount["en"] || 0
47+
const completeness: LanguageCompletion = {}
48+
49+
languages.forEach((lang) => {
50+
if (lang !== "en") {
51+
const percent = Math.round((keyCount[lang] / enCount) * 100)
52+
completeness[lang] = percent
53+
}
54+
})
55+
56+
return completeness
57+
}
58+
59+
const i18n = calculateCompleteness(path.resolve(__dirname, "../locales"))
60+
export default i18n

configs/vite.render.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { prerelease } from "semver"
1111
import type { Plugin, UserConfig } from "vite"
1212

1313
import { getGitHash } from "../scripts/lib"
14+
import i18nCompleteness from "./i18n-completeness"
1415

1516
const pkg = JSON.parse(readFileSync("package.json", "utf8"))
1617
const isCI = process.env.CI === "true" || process.env.CI === "1"
@@ -134,6 +135,8 @@ export const viteRenderBaseConfig = {
134135
RELEASE_CHANNEL: JSON.stringify((prerelease(pkg.version)?.[0] as string) || "stable"),
135136

136137
DEBUG: process.env.DEBUG === "true",
138+
139+
I18N_COMPLETENESS_MAP: JSON.stringify({ ...i18nCompleteness, en: 100 }),
137140
},
138141
} satisfies UserConfig
139142

locales/app/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"ai_daily.title": "Top News - {{title}}",
33
"ai_daily.tooltip.content": "Here is news selected by AI from your timeline (<From /> - <To />) that may be important to you.",
44
"ai_daily.tooltip.update_schedule": "Update daily at 8 AM and 8 PM.",
5+
"app.app_name": "APP_NAME",
6+
"app.copy_logo_svg": "Copy Logo SVG",
7+
"app.toggle_sidebar": "Toggle Sidebar",
58
"discover.any_url_or_keyword": "Any URL or Keyword",
69
"discover.default_option": " (default)",
710
"discover.feed_description": "The description of this feed is as follows, and you can fill out the parameter form with the relevant information.",

locales/app/zh-CN.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"ai_daily.title": "头条 - {{title}}",
33
"ai_daily.tooltip.content": "这里是通过 AI 从您的时间线中选择的头条新闻(<From /> - <To />),可能对您很重要。",
44
"ai_daily.tooltip.update_schedule": "每天早上 8 点、晚上 8 点更新。",
5+
"app.app_name": "APP_NAME",
6+
"app.copy_logo_svg": "复制 Logo SVG",
7+
"app.toggle_sidebar": "切换侧边栏",
58
"discover.any_url_or_keyword": "任何 URL 或关键词",
69
"discover.default_option": " (默认)",
710
"discover.feed_description": "此 Feed 的描述如下,您可以填写相关信息。",

locales/common/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"app.copied_to_clipboard": "Copied to clipboard",
23
"cancel": "Cancel",
34
"confirm": "Confirm",
45
"ok": "OK",

locales/common/ja.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"app.copied_to_clipboard": "クリップボードにコピーしました",
3+
"cancel": "キャンセル",
4+
"confirm": "確認",
5+
"ok": "OK",
6+
"quantifier.piece": "",
7+
"time.last_night": "昨夜",
8+
"time.the_night_before_last": "一昨夜",
9+
"time.today": "今日",
10+
"time.yesterday": "昨日",
11+
"tips.load-lng-error": "言語パックの読み込みに失敗しました",
12+
"words.back": "戻る",
13+
"words.copy": "コピー",
14+
"words.edit": "編集",
15+
"words.entry": "エントリー",
16+
"words.id": "ID",
17+
"words.items_one": "アイテム",
18+
"words.items_other": "アイテム",
19+
"words.local": "ローカル",
20+
"words.record": "記録",
21+
"words.record_one": "記録",
22+
"words.record_other": "記録",
23+
"words.result": "結果",
24+
"words.result_one": "結果",
25+
"words.result_other": "結果",
26+
"words.space": "",
27+
"words.which.all": "すべて"
28+
}

locales/common/zh-CN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"app.copied_to_clipboard": "已复制到剪贴板",
23
"cancel": "取消",
34
"confirm": "确认",
45
"ok": "",

locales/lang/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"langs.en": "English",
3-
"langs.zh-CN": "简体中文(部分完成)",
3+
"langs.ja": "日本語",
4+
"langs.zh-CN": "简体中文",
45
"name": "English"
56
}

locales/lang/ja.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"langs.en": "English",
3+
"langs.ja": "日本語",
4+
"langs.zh-CN": "简体中文",
5+
"name": "日本語"
6+
}

locales/lang/zh-CN.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"langs.en": "English",
3-
"langs.zh-CN": "简体中文(部分完成)",
4-
"name": "English"
3+
"langs.ja": "日本語",
4+
"langs.zh-CN": "简体中文",
5+
"name": "简体中文"
56
}

src/renderer/src/@types/constants.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
export const currentSupportedLanguages = (() => {
2-
const langsFiles = import.meta.glob("../../../../locales/app/*.json")
3-
4-
const langs = [] as string[]
5-
for (const key in langsFiles) {
6-
langs.push(key.split("/").pop()?.replace(".json", "") as string)
7-
}
8-
return langs
9-
})()
1+
export const currentSupportedLanguages = ["en", "ja", "zh-CN"]
102

113
export const dayjsLocaleImportMap = {
124
en: ["en", () => import("dayjs/locale/en")],
135
["zh-CN"]: ["zh-cn", () => import("dayjs/locale/zh-cn")],
6+
["ja"]: ["ja", () => import("dayjs/locale/ja")],
147
}
158

169
export const ns = ["app", "common", "lang", "settings", "shortcuts"] as const

src/renderer/src/@types/default-resource.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import en from "../../../../locales/app/en.json"
22
import common_en from "../../../../locales/common/en.json"
3+
import common_ja from "../../../../locales/common/ja.json"
34
import common_zhCN from "../../../../locales/common/zh-CN.json"
45
import external_en from "../../../../locales/external/en.json"
56
import lang_en from "../../../../locales/lang/en.json"
7+
import lang_ja from "../../../../locales/lang/ja.json"
68
import lang_zhCN from "../../../../locales/lang/zh-CN.json"
79
import settings_en from "../../../../locales/settings/en.json"
810
import shortcuts_en from "../../../../locales/shortcuts/en.json"
@@ -28,4 +30,8 @@ export const defaultResources = {
2830
lang: lang_zhCN,
2931
common: common_zhCN,
3032
},
33+
ja: {
34+
lang: lang_ja,
35+
common: common_ja,
36+
},
3137
}

src/renderer/src/env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
declare const APP_VERSION: string
33
declare const APP_NAME: string
44
declare const RELEASE_CHANNEL: string
5+
declare const I18N_COMPLETENESS_MAP: Record<string, number>

src/renderer/src/modules/entry-column/components/mark-all-button.tsx

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { PopoverPortal } from "@radix-ui/react-popover"
21
import { ActionButton, Button, IconButton } from "@renderer/components/ui/button"
32
import { Kbd, KbdCombined } from "@renderer/components/ui/kbd/Kbd"
4-
import { Popover, PopoverContent, PopoverTrigger } from "@renderer/components/ui/popover"
53
import { RootPortal } from "@renderer/components/ui/portal"
64
import { HotKeyScopeMap } from "@renderer/constants"
75
import { shortcuts } from "@renderer/constants/shortcuts"
@@ -176,65 +174,6 @@ const ConfirmMarkAllReadInfo = ({ undo }: { undo: () => any }) => {
176174
)
177175
}
178176

179-
/**
180-
* @deprecated
181-
*/
182-
export const MarkAllReadPopover = forwardRef<HTMLButtonElement, MarkAllButtonProps>(
183-
({ filter, className, which = "all", shortcut }, ref) => {
184-
const [markPopoverOpen, setMarkPopoverOpen] = useState(false)
185-
186-
const handleMarkAllAsRead = useMarkAllByRoute(filter)
187-
188-
return (
189-
<Popover open={markPopoverOpen} onOpenChange={setMarkPopoverOpen}>
190-
<PopoverTrigger asChild>
191-
<ActionButton
192-
shortcut={shortcut ? shortcuts.entries.markAllAsRead.key : undefined}
193-
tooltip={
194-
<span>
195-
Mark
196-
<span> </span>
197-
{which}
198-
<span> </span>
199-
as read
200-
</span>
201-
}
202-
className={className}
203-
ref={ref}
204-
>
205-
<i className="i-mgc-check-circle-cute-re" />
206-
</ActionButton>
207-
</PopoverTrigger>
208-
<PopoverPortal>
209-
<PopoverContent className="flex w-fit flex-col items-center justify-center gap-3 !py-3 [&_button]:text-xs">
210-
<div className="text-sm">
211-
<Trans
212-
i18nKey="mark_all_read_button.mark_as_read"
213-
values={{
214-
// @ts-expect-error https://www.i18next.com/overview/typescript#type-error-template-literal
215-
// should be fixed by using `as const` but it's not working
216-
which: commonT(`words.which.${which}`),
217-
}}
218-
/>
219-
</div>
220-
<div className="space-x-4">
221-
<IconButton
222-
icon={<i className="i-mgc-check-filled" />}
223-
onClick={() => {
224-
handleMarkAllAsRead()
225-
setMarkPopoverOpen(false)
226-
}}
227-
>
228-
Confirm
229-
</IconButton>
230-
</div>
231-
</PopoverContent>
232-
</PopoverPortal>
233-
</Popover>
234-
)
235-
},
236-
)
237-
238177
export const FlatMarkAllReadButton: FC<MarkAllButtonProps> = (props) => {
239178
const t = useI18n()
240179

src/renderer/src/modules/feed-column/header.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ActionButton } from "@renderer/components/ui/button"
66
import { Popover, PopoverContent, PopoverTrigger } from "@renderer/components/ui/popover"
77
import { ProfileButton } from "@renderer/components/user-button"
88
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
9+
import { useI18n } from "@renderer/hooks/common"
910
import { stopPropagation } from "@renderer/lib/dom"
1011
import { cn } from "@renderer/lib/utils"
1112
import { m } from "framer-motion"
@@ -53,7 +54,6 @@ export const FeedColumnHeader = memo(() => {
5354
}}
5455
>
5556
<Logo className="mr-1 size-6" />
56-
5757
{APP_NAME}
5858
</div>
5959
</LogoContextMenu>
@@ -80,10 +80,11 @@ const LayoutActionButton = () => {
8080
width: !feedColumnShow ? "auto" : 0,
8181
})
8282

83+
const t = useI18n()
8384
return (
8485
<m.div initial={animation} animate={animation} className="overflow-hidden">
8586
<ActionButton
86-
tooltip="Toggle Sidebar"
87+
tooltip={t("app.toggle_sidebar")}
8788
icon={
8889
<i
8990
className={cn(
@@ -103,6 +104,7 @@ const LayoutActionButton = () => {
103104
const LogoContextMenu: FC<PropsWithChildren> = ({ children }) => {
104105
const [open, setOpen] = useState(false)
105106
const logoRef = useRef<SVGSVGElement>(null)
107+
const t = useI18n()
106108

107109
return (
108110
<Popover open={open} onOpenChange={setOpen}>
@@ -120,7 +122,7 @@ const LogoContextMenu: FC<PropsWithChildren> = ({ children }) => {
120122
onClick={() => {
121123
navigator.clipboard.writeText(logoRef.current?.outerHTML || "")
122124
setOpen(false)
123-
toast.success("Copied to clipboard")
125+
toast.success(t.common("app.copied_to_clipboard"))
124126
}}
125127
className={cn(
126128
"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<PropsWithChildren> = ({ children }) => {
129131
)}
130132
>
131133
<Logo ref={logoRef} />
132-
<span>Copy Logo SVG</span>
134+
<span>{t("app.copy_logo_svg")}</span>
133135
</button>
134136
</PopoverContent>
135137
</Popover>

src/renderer/src/modules/settings/tabs/general.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,11 +221,16 @@ export const LanguageSelector = () => {
221221
<SelectValue />
222222
</SelectTrigger>
223223
<SelectContent position="item-aligned">
224-
{currentSupportedLanguages.map((lang) => (
225-
<SelectItem key={lang} value={lang}>
226-
{langT(`langs.${lang}` as any)}
227-
</SelectItem>
228-
))}
224+
{currentSupportedLanguages.map((lang) => {
225+
const percent = I18N_COMPLETENESS_MAP[lang]
226+
227+
return (
228+
<SelectItem key={lang} value={lang}>
229+
{langT(`langs.${lang}` as any)}{" "}
230+
{typeof percent === "number" ? (percent === 100 ? null : `(${percent}%)`) : null}
231+
</SelectItem>
232+
)
233+
})}
229234
</SelectContent>
230235
</Select>
231236
</div>

tsconfig.node.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"src/env.ts",
1010
"./src/hono.ts",
1111
"src/shared/src/global.d.ts",
12-
"configs/vite.render.config.ts",
12+
"configs/*",
1313
"./scripts/**/*"
1414
],
1515
"compilerOptions": {

0 commit comments

Comments
 (0)