feat: language indicator
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
d81c2b5791
commit
bf7da0431f
|
|
@ -0,0 +1,60 @@
|
|||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
type LanguageCompletion = Record<string, number>
|
||||
|
||||
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<string>()
|
||||
const keyCount: Record<string, number> = {}
|
||||
|
||||
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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
"ai_daily.title": "Top News - {{title}}",
|
||||
"ai_daily.tooltip.content": "Here is news selected by AI from your timeline (<From /> - <To />) 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.",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
"ai_daily.title": "头条 - {{title}}",
|
||||
"ai_daily.tooltip.content": "这里是通过 AI 从您的时间线中选择的头条新闻(<From /> - <To />),可能对您很重要。",
|
||||
"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 的描述如下,您可以填写相关信息。",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"app.copied_to_clipboard": "Copied to clipboard",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"ok": "OK",
|
||||
|
|
|
|||
|
|
@ -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": "すべて"
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"app.copied_to_clipboard": "已复制到剪贴板",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"ok": "好",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"langs.en": "English",
|
||||
"langs.zh-CN": "简体中文(部分完成)",
|
||||
"langs.ja": "日本語",
|
||||
"langs.zh-CN": "简体中文",
|
||||
"name": "English"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"langs.en": "English",
|
||||
"langs.ja": "日本語",
|
||||
"langs.zh-CN": "简体中文",
|
||||
"name": "日本語"
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"langs.en": "English",
|
||||
"langs.zh-CN": "简体中文(部分完成)",
|
||||
"name": "English"
|
||||
"langs.ja": "日本語",
|
||||
"langs.zh-CN": "简体中文",
|
||||
"name": "简体中文"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, number>
|
||||
|
|
|
|||
|
|
@ -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<HTMLButtonElement, MarkAllButtonProps>(
|
||||
({ filter, className, which = "all", shortcut }, ref) => {
|
||||
const [markPopoverOpen, setMarkPopoverOpen] = useState(false)
|
||||
|
||||
const handleMarkAllAsRead = useMarkAllByRoute(filter)
|
||||
|
||||
return (
|
||||
<Popover open={markPopoverOpen} onOpenChange={setMarkPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<ActionButton
|
||||
shortcut={shortcut ? shortcuts.entries.markAllAsRead.key : undefined}
|
||||
tooltip={
|
||||
<span>
|
||||
Mark
|
||||
<span> </span>
|
||||
{which}
|
||||
<span> </span>
|
||||
as read
|
||||
</span>
|
||||
}
|
||||
className={className}
|
||||
ref={ref}
|
||||
>
|
||||
<i className="i-mgc-check-circle-cute-re" />
|
||||
</ActionButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverPortal>
|
||||
<PopoverContent className="flex w-fit flex-col items-center justify-center gap-3 !py-3 [&_button]:text-xs">
|
||||
<div className="text-sm">
|
||||
<Trans
|
||||
i18nKey="mark_all_read_button.mark_as_read"
|
||||
values={{
|
||||
// @ts-expect-error https://www.i18next.com/overview/typescript#type-error-template-literal
|
||||
// should be fixed by using `as const` but it's not working
|
||||
which: commonT(`words.which.${which}`),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-x-4">
|
||||
<IconButton
|
||||
icon={<i className="i-mgc-check-filled" />}
|
||||
onClick={() => {
|
||||
handleMarkAllAsRead()
|
||||
setMarkPopoverOpen(false)
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</IconButton>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</Popover>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const FlatMarkAllReadButton: FC<MarkAllButtonProps> = (props) => {
|
||||
const t = useI18n()
|
||||
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
|||
}}
|
||||
>
|
||||
<Logo className="mr-1 size-6" />
|
||||
|
||||
{APP_NAME}
|
||||
</div>
|
||||
</LogoContextMenu>
|
||||
|
|
@ -80,10 +80,11 @@ const LayoutActionButton = () => {
|
|||
width: !feedColumnShow ? "auto" : 0,
|
||||
})
|
||||
|
||||
const t = useI18n()
|
||||
return (
|
||||
<m.div initial={animation} animate={animation} className="overflow-hidden">
|
||||
<ActionButton
|
||||
tooltip="Toggle Sidebar"
|
||||
tooltip={t("app.toggle_sidebar")}
|
||||
icon={
|
||||
<i
|
||||
className={cn(
|
||||
|
|
@ -103,6 +104,7 @@ const LayoutActionButton = () => {
|
|||
const LogoContextMenu: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const logoRef = useRef<SVGSVGElement>(null)
|
||||
const t = useI18n()
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
|
|
@ -120,7 +122,7 @@ const LogoContextMenu: FC<PropsWithChildren> = ({ 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<PropsWithChildren> = ({ children }) => {
|
|||
)}
|
||||
>
|
||||
<Logo ref={logoRef} />
|
||||
<span>Copy Logo SVG</span>
|
||||
<span>{t("app.copy_logo_svg")}</span>
|
||||
</button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
|
|
|||
|
|
@ -221,11 +221,16 @@ export const LanguageSelector = () => {
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
{currentSupportedLanguages.map((lang) => (
|
||||
<SelectItem key={lang} value={lang}>
|
||||
{langT(`langs.${lang}` as any)}
|
||||
</SelectItem>
|
||||
))}
|
||||
{currentSupportedLanguages.map((lang) => {
|
||||
const percent = I18N_COMPLETENESS_MAP[lang]
|
||||
|
||||
return (
|
||||
<SelectItem key={lang} value={lang}>
|
||||
{langT(`langs.${lang}` as any)}{" "}
|
||||
{typeof percent === "number" ? (percent === 100 ? null : `(${percent}%)`) : null}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"src/env.ts",
|
||||
"./src/hono.ts",
|
||||
"src/shared/src/global.d.ts",
|
||||
"configs/vite.render.config.ts",
|
||||
"configs/*",
|
||||
"./scripts/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue