From 267f6b5b1cc302ed5e40cafd31ac9888324bd1f7 Mon Sep 17 00:00:00 2001 From: buzhidaoqsm <1195212504@qq.com> Date: Thu, 23 Apr 2026 15:48:08 +0800 Subject: [PATCH] feat: add Chinese localization support --- package-lock.json | 86 +++++++++++++++++++- package.json | 2 + src/components/CustomPathCard.tsx | 35 ++++---- src/components/FullDiskCard.tsx | 22 ++--- src/components/HistoryToggle.tsx | 6 +- src/components/NotificationCenter.tsx | 10 ++- src/components/ScanTabs.tsx | 6 +- src/components/SpaceHistoryChart.tsx | 24 +++--- src/components/custom_path.tsx | 8 +- src/components/data_table.tsx | 4 +- src/components/data_table_columns.tsx | 16 ++-- src/components/disk_path.tsx | 6 +- src/components/overview-tab.tsx | 37 +++++---- src/components/sheet-demo.tsx | 69 ++++++++++++---- src/components/splash_page.tsx | 9 ++- src/components/top-bar.tsx | 6 +- src/components/tree-view.tsx | 26 +++--- src/i18n/index.ts | 36 +++++++++ src/i18n/languages.ts | 31 +++++++ src/i18n/locales/en.json | 111 ++++++++++++++++++++++++++ src/i18n/locales/zh-CN.json | 111 ++++++++++++++++++++++++++ src/lib/utils.ts | 6 +- src/main.tsx | 1 + 23 files changed, 553 insertions(+), 115 deletions(-) create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/languages.ts create mode 100644 src/i18n/locales/en.json create mode 100644 src/i18n/locales/zh-CN.json diff --git a/package-lock.json b/package-lock.json index f2ee4b1..0848f61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "filesize": "^11.0.13", + "i18next": "^26.0.6", "immer": "^10.2.0", "lucide-react": "^0.548.0", "next-themes": "^0.4.6", @@ -38,6 +39,7 @@ "react": "^18.3.1", "react-arborist": "^3.4.3", "react-dom": "^18.3.1", + "react-i18next": "^17.0.4", "recharts": "^2.15.4", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", @@ -292,9 +294,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -4067,6 +4069,46 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.0.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.6.tgz", + "integrity": "sha512-A4U6eCXodIbrhf8EarRurB9/4ebyaurH4+fu4gig9bqxmpSt+fCAFm/GpRQDcN1Xzu/LdFCx4nYHsnM1edIIbg==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/immer": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", @@ -4770,6 +4812,33 @@ "react": "^18.3.1" } }, + "node_modules/react-i18next": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.4.tgz", + "integrity": "sha512-hQipmK4EF0y6RO6tt6WuqnmWpWYEXmQUUzecmMBuNsIgYd3smXcG4GtYPWhvgxn0pqMOItKlEO8H24HCs5hc3g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5083,7 +5152,7 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5302,6 +5371,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index b7b247a..f7db69e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "filesize": "^11.0.13", + "i18next": "^26.0.6", "immer": "^10.2.0", "lucide-react": "^0.548.0", "next-themes": "^0.4.6", @@ -40,6 +41,7 @@ "react": "^18.3.1", "react-arborist": "^3.4.3", "react-dom": "^18.3.1", + "react-i18next": "^17.0.4", "recharts": "^2.15.4", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", diff --git a/src/components/CustomPathCard.tsx b/src/components/CustomPathCard.tsx index e531c62..826f939 100644 --- a/src/components/CustomPathCard.tsx +++ b/src/components/CustomPathCard.tsx @@ -1,6 +1,5 @@ -import React, { useState, useEffect } from 'react' -import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card' -import DiskPath from './disk_path' +import React, { useState } from 'react' +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from './ui/card' import { Separator } from '@/components/ui/separator' import CustomPath from './custom_path' import { Button } from './ui/button' @@ -8,18 +7,11 @@ import { Checkbox } from './ui/checkbox' import { Label } from '@/components/ui/label' import { invoke } from '@tauri-apps/api/core'; -import { snapshotStore, useErrorStore, userStore } from './store' -import { DataTable } from './data_table' -import { SnapshotFile } from './data_table_columns' - -import { columns } from './data_table_columns' -import { RowSelectionState } from '@tanstack/react-table' +import { useErrorStore, userStore } from './store' import Progress from './progress' -import DeltaLogo from '../../src-tauri/icons/64x64.png' -import { DirView, InitDisk } from '@/types' -import TopBar from './top-bar' -import { ScanTabs } from './ScanTabs' +import { DirView } from '@/types' +import { useTranslation } from 'react-i18next' interface SplashPageProps { setWhichField: React.Dispatch>; @@ -27,6 +19,7 @@ interface SplashPageProps { const CustomPathCard: React.FC = ({ setWhichField }) => { + const { t } = useTranslation() // Card only needs to read the currently selected snapshot file global store obj? const [selectedPath, setSelectedPath] = useState(""); @@ -54,11 +47,11 @@ const CustomPathCard: React.FC = ({ setWhichField }) => { return ( - Start Snapshot + {t("scan.title")}
-

Custom Path

+

{t("scan.fields.customPath")}

@@ -67,9 +60,9 @@ const CustomPathCard: React.FC = ({ setWhichField }) => {
- +

- Compare current scan with previous scanned snapshot + {t("scan.options.compareSnapshotsDescription")}

@@ -79,9 +72,9 @@ const CustomPathCard: React.FC = ({ setWhichField }) => {
- +

- Save current scan into new snapshot file + {t("scan.options.saveSnapshotDescription")}

@@ -90,7 +83,7 @@ const CustomPathCard: React.FC = ({ setWhichField }) => {
- +
@@ -98,4 +91,4 @@ const CustomPathCard: React.FC = ({ setWhichField }) => { ) } -export default CustomPathCard \ No newline at end of file +export default CustomPathCard diff --git a/src/components/FullDiskCard.tsx b/src/components/FullDiskCard.tsx index e602f30..9e0d8ed 100644 --- a/src/components/FullDiskCard.tsx +++ b/src/components/FullDiskCard.tsx @@ -13,6 +13,7 @@ import { snapshotStore, useErrorStore, userStore } from './store' import Progress from './progress' import { DirView, InitDisk } from '@/types' +import { useTranslation } from 'react-i18next' interface SplashPageProps { @@ -21,6 +22,7 @@ interface SplashPageProps { const FullDiskCard: React.FC = ({ setWhichField }) => { + const { t } = useTranslation() const [disks, setDisks] = useState([]); @@ -46,12 +48,12 @@ const FullDiskCard: React.FC = ({ setWhichField }) => { } catch (err) { setCurrentBackendError(err) - setDisks([{ name: "Unknown", desc: "No Disks Found" }]) + setDisks([{ name: t("scan.fallback.unknownDisk"), desc: t("scan.fallback.noDisksFound") }]) } } getDisks(); - }, []); + }, [setCurrentBackendError, t]); const runScan = async (target: string) => { try { @@ -81,11 +83,11 @@ const FullDiskCard: React.FC = ({ setWhichField }) => { return ( - Start Snapshot + {t("scan.title")}
-

Disk

+

{t("scan.fields.disk")}

@@ -94,9 +96,9 @@ const FullDiskCard: React.FC = ({ setWhichField }) => {
- +

- Compare current scan with previous scanned snapshot + {t("scan.options.compareSnapshotsDescription")}

@@ -106,9 +108,9 @@ const FullDiskCard: React.FC = ({ setWhichField }) => {
setSaveCurrentSnapshotFlag(checked === true)} /*<-- Typescript type check shi*/ />
- +

- Save current scan into new snapshot file + {t("scan.options.saveSnapshotDescription")}

@@ -116,7 +118,7 @@ const FullDiskCard: React.FC = ({ setWhichField }) => {
- +
@@ -124,4 +126,4 @@ const FullDiskCard: React.FC = ({ setWhichField }) => { ) } -export default FullDiskCard \ No newline at end of file +export default FullDiskCard diff --git a/src/components/HistoryToggle.tsx b/src/components/HistoryToggle.tsx index 9be4eb7..859d4e7 100644 --- a/src/components/HistoryToggle.tsx +++ b/src/components/HistoryToggle.tsx @@ -2,8 +2,10 @@ import { Label } from "@/components/ui/label" import { Switch } from "@/components/ui/switch" import { useConfigurationStore } from "./store" import { Activity } from "lucide-react" +import { useTranslation } from "react-i18next" export function HistoryToggle() { + const { t } = useTranslation() const setShowHistory = useConfigurationStore((state) => state.setShowHistory) const showHistoryFlag = useConfigurationStore((state) => state.ShowHistory) @@ -11,10 +13,10 @@ export function HistoryToggle() {

- Enable the historical size graph when viewing directory entries. + {t("settings.historyGraphDescription")}

state.currentBackendErrors) const [unreadCount, setUnreadCount] = useState(0) @@ -48,11 +50,11 @@ export function NotificationCenter() { {unreadCount} )} - Toggle notifications + {t("notifications.toggle")} -
Notifications
+
{t("notifications.title")}
{backendErrorList.map((notification, index) => ( @@ -61,7 +63,7 @@ export function NotificationCenter() { className="flex flex-col gap-1 rounded-md p-3 text-xs transition-colors hover:bg-muted/50 bg-muted/20 border border-red-500/30 overflow-hidden" >
- {`Error: ${notification.err_code}`} + {t("notifications.errorCode", { code: notification.err_code })}
{notification.library_generated_error_desc?.toLowerCase() !== "n/a" && (
@@ -80,4 +82,4 @@ export function NotificationCenter() { ) -} \ No newline at end of file +} diff --git a/src/components/ScanTabs.tsx b/src/components/ScanTabs.tsx index bea3e31..3e76790 100644 --- a/src/components/ScanTabs.tsx +++ b/src/components/ScanTabs.tsx @@ -19,17 +19,19 @@ import { Button } from "./ui/button" import Progress from './progress' import FullDiskCard from "./FullDiskCard" import CustomPathCard from "./CustomPathCard" +import { useTranslation } from "react-i18next" interface SplashPageProps { setWhichField: React.Dispatch>; } export function ScanTabs({ setWhichField }) { + const { t } = useTranslation() return ( - Full Disk - Custom Path + {t("scan.tabs.fullDisk")} + {t("scan.tabs.customPath")} diff --git a/src/components/SpaceHistoryChart.tsx b/src/components/SpaceHistoryChart.tsx index 5c21907..f9af18b 100644 --- a/src/components/SpaceHistoryChart.tsx +++ b/src/components/SpaceHistoryChart.tsx @@ -2,33 +2,35 @@ import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts" import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" import { formatBytes } from "@/lib/utils" import { useConfigurationStore, useDirEntryHistoryStore } from "./store" - -const chartConfig = { - size: { - label: "Size", - color: "hsla(273, 98%, 25%, 1.00)", - }, -} +import { useTranslation } from "react-i18next" import { Card } from "@/components/ui/card" export function SpaceHistoryChart() { + const { t, i18n } = useTranslation(); const chartData = useDirEntryHistoryStore((s) => s.currentDirEntryHistory); const showHistoryFlag = useConfigurationStore((state) => state.ShowHistory); + const locale = i18n.resolvedLanguage ?? i18n.language; + const chartConfig = { + size: { + label: t("history.chartLabel"), + color: "hsla(273, 98%, 25%, 1.00)", + }, + }; const renderContent = () => { if (!showHistoryFlag) { return (
- History graph disabled. + {t("history.disabled")}
) } if (!chartData || chartData.length === 0) { return (
- Select a directory or file to view history. + {t("history.empty")}
); } @@ -77,7 +79,7 @@ export function SpaceHistoryChart() { labelFormatter={(_, payload) => { if (!payload?.[0]?.payload?.timestamp) return ""; const d = new Date(payload[0].payload.timestamp); - return d.toLocaleDateString(undefined, { timeZone: "UTC" }); + return d.toLocaleDateString(locale, { timeZone: "UTC" }); }} formatter={(value) => formatBytes(value)} /> @@ -103,4 +105,4 @@ export function SpaceHistoryChart() {
); -} \ No newline at end of file +} diff --git a/src/components/custom_path.tsx b/src/components/custom_path.tsx index d0f8047..12e1bf2 100644 --- a/src/components/custom_path.tsx +++ b/src/components/custom_path.tsx @@ -4,6 +4,7 @@ import { Button } from './ui/button' import { FolderSearch } from 'lucide-react' import { open } from '@tauri-apps/plugin-dialog' import { useErrorStore } from './store' +import { useTranslation } from 'react-i18next' interface CustomPathProps { value: string; @@ -11,6 +12,7 @@ interface CustomPathProps { } const CustomPath: React.FC = ({ value, onChange }) => { + const { t } = useTranslation() const setCurrentBackendError = useErrorStore((state) => state.setCurrentBackendError) @@ -35,13 +37,13 @@ const CustomPath: React.FC = ({ value, onChange }) => { onChange(e.target.value)} - placeholder="Enter custom path..." + placeholder={t("scan.placeholders.customPath")} /> -
) } -export default CustomPath \ No newline at end of file +export default CustomPath diff --git a/src/components/data_table.tsx b/src/components/data_table.tsx index b958fbc..c6d4052 100644 --- a/src/components/data_table.tsx +++ b/src/components/data_table.tsx @@ -33,6 +33,7 @@ import { ScrollArea } from "./ui/scroll-area" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table" import { ColumnDef, flexRender, getCoreRowModel, OnChangeFn, RowSelectionState, useReactTable } from "@tanstack/react-table" +import { useTranslation } from "react-i18next" interface DataTableProps { columns: ColumnDef[] @@ -43,6 +44,7 @@ interface DataTableProps { // making the datatable generic with the input types export function DataTable({columns, data, rowSelection, setRowSelection}: DataTableProps) { + const { t } = useTranslation() const table = useReactTable({ data, columns, @@ -104,7 +106,7 @@ export function DataTable({columns, data, rowSelection, setRowSel ( - No snapshot files. + {t("snapshot.empty")} ) diff --git a/src/components/data_table_columns.tsx b/src/components/data_table_columns.tsx index 6029e6d..b26ca32 100644 --- a/src/components/data_table_columns.tsx +++ b/src/components/data_table_columns.tsx @@ -1,6 +1,7 @@ import { ColumnDef } from "@tanstack/react-table" import { Checkbox } from "./ui/checkbox" import { formatBytes, formatDateTime } from "@/lib/utils" +import { TFunction } from "i18next" export type SnapshotFile = { drive_letter: string @@ -9,10 +10,13 @@ export type SnapshotFile = { size: number, } -export const columns: ColumnDef[] = [ +export const createSnapshotColumns = ( + t: TFunction, + locale: string +): ColumnDef[] => [ { accessorKey: "driveLetter", - header: "Drive Name", + header: t("snapshot.columns.driveName"), cell: ({ row }) => { // Cell is basically how the actual value is rendered return
row.toggleSelected(!!value)}> @@ -22,18 +26,18 @@ export const columns: ColumnDef[] = [ }, { accessorKey: "dateKey", // Header is Date but the accessor key is the number date (for sorting) - header: "Date", + header: t("snapshot.columns.date"), cell: ({ row }) => { - return
{formatDateTime(row.original.date_time)}
+ return
{formatDateTime(row.original.date_time, locale)}
} }, { accessorKey: "size", - header: "Size", + header: t("snapshot.columns.size"), cell: ({ row }) => { return

{formatBytes(row.original.size)}

} } -] \ No newline at end of file +] diff --git a/src/components/disk_path.tsx b/src/components/disk_path.tsx index 3aa2b20..f924bf4 100644 --- a/src/components/disk_path.tsx +++ b/src/components/disk_path.tsx @@ -8,6 +8,7 @@ import { SelectValue, } from "@/components/ui/select" import { InitDisk } from "@/types"; +import { useTranslation } from "react-i18next"; interface DiskPathProp { @@ -19,11 +20,12 @@ interface DiskPathProp { // NOTE THAT FOR SHADE CN THE VALUEPROP PASSED TO THE SLECET ITEM IS THE KEY IT MANAGE IT ITSELF!! // THAT ALSO creates repated slectitems each with the value and the value is waht is propogated up on change const DiskPath = ({ disks, onValueChange }: DiskPathProp) => { + const { t } = useTranslation() return ( <> i18n.changeLanguage(language)} + > + + + + + + {supportedLanguages.map((language) => ( + + {language.nativeLabel} + + ))} + + + +
+
@@ -182,11 +223,11 @@ export function SettingsPage() { - + ) -} \ No newline at end of file +} diff --git a/src/components/splash_page.tsx b/src/components/splash_page.tsx index 7ab544e..4a40278 100644 --- a/src/components/splash_page.tsx +++ b/src/components/splash_page.tsx @@ -12,7 +12,7 @@ import { snapshotStore, useErrorStore, userStore } from './store' import { DataTable } from './data_table' import { SnapshotFile } from './data_table_columns' -import { columns } from './data_table_columns' +import { createSnapshotColumns } from './data_table_columns' import { RowSelectionState } from '@tanstack/react-table' import Progress from './progress' @@ -20,12 +20,15 @@ import DeltaLogo from '../../src-tauri/icons/64x64.png' import { DirView, InitDisk } from '@/types' import TopBar from './top-bar' import { ScanTabs } from './ScanTabs' +import { useTranslation } from 'react-i18next' interface SplashPageProps { setWhichField: React.Dispatch>; } const SplashPage: React.FC = ({ setWhichField }) => { + const { t, i18n } = useTranslation() + const columns = createSnapshotColumns(t, i18n.resolvedLanguage ?? i18n.language) // const [disks, setDisks] = useState([]); @@ -86,7 +89,7 @@ const SplashPage: React.FC = ({ setWhichField }) => {
{/* Temp image */} - This the App Logo + {t("app.logoAlt")} {/* Test data table for snapshots, datatable should be generic */} @@ -101,4 +104,4 @@ const SplashPage: React.FC = ({ setWhichField }) => { ) } -export default SplashPage \ No newline at end of file +export default SplashPage diff --git a/src/components/top-bar.tsx b/src/components/top-bar.tsx index aa5c084..ff2f713 100644 --- a/src/components/top-bar.tsx +++ b/src/components/top-bar.tsx @@ -4,12 +4,14 @@ import { SettingsPage } from './sheet-demo' import { NotificationCenter } from './NotificationCenter' import { Button } from './ui/button' import { ArrowLeft } from 'lucide-react' +import { useTranslation } from 'react-i18next' interface TopBarProps { onHomeClick?: () => void; } const TopBar = ({ onHomeClick }: TopBarProps) => { + const { t } = useTranslation() const currentPath = userStore((state) => state.currentPath) const currentEntryDetails = userStore((state) => state.currentEntryDetail) let numsubdir = currentEntryDetails.numsubdir ? String(currentEntryDetails.numsubdir) : ("0"); @@ -28,7 +30,7 @@ const TopBar = ({ onHomeClick }: TopBarProps) => {
- {`Subdir: ${numsubdir} Subfiles: ${numsubfile}`} + {`${t("topBar.subdir")}: ${numsubdir} ${t("topBar.subfiles")}: ${numsubfile}`} @@ -46,4 +48,4 @@ const TopBar = ({ onHomeClick }: TopBarProps) => { ) } -export default TopBar \ No newline at end of file +export default TopBar diff --git a/src/components/tree-view.tsx b/src/components/tree-view.tsx index d47e928..2795320 100644 --- a/src/components/tree-view.tsx +++ b/src/components/tree-view.tsx @@ -7,6 +7,7 @@ import { Badge } from "./ui/badge"; import InfoFlagBar from "./info_flag_bar"; import { formatBytes, parsePathToSegment, pathSeparator } from "../lib/utils" import { Progress } from "./ui/progress"; +import { useTranslation } from "react-i18next"; const INDENT_SIZE = 20; @@ -19,16 +20,20 @@ const COL_WIDTHS = { }; // Header -const TreeHeader = () => ( -
-
Name
+const TreeHeader = () => { + const { t } = useTranslation(); -
Size
-
Prev
-
Change
-
Flag
-
-); + return ( +
+
{t("tree.headers.name")}
+ +
{t("tree.headers.size")}
+
{t("tree.headers.previous")}
+
{t("tree.headers.change")}
+
{t("tree.headers.flag")}
+
+ ); +}; const SimpleNode = ({ node, style, dragHandle }: any) => { @@ -168,10 +173,11 @@ const SimpleNode = ({ node, style, dragHandle }: any) => { }; function TestFileTreeSecond() { + const { t } = useTranslation(); const rootState = userStore((state) => state.root); const { ref, width, height } = useResizeObserver(); - if (!rootState) return
Loading...
; + if (!rootState) return
{t("common.loading")}
; return (
diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..2b615c2 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,36 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import en from "./locales/en.json"; +import zhCN from "./locales/zh-CN.json"; +import { + fallbackLanguage, + LANGUAGE_STORAGE_KEY, + normalizeLanguage, +} from "./languages"; + +const savedLanguage = normalizeLanguage( + localStorage.getItem(LANGUAGE_STORAGE_KEY) ?? undefined +); +const systemLanguage = normalizeLanguage(navigator.language); + +i18n.use(initReactI18next).init({ + resources: { + en: { translation: en }, + "zh-CN": { translation: zhCN }, + }, + lng: savedLanguage ?? systemLanguage ?? fallbackLanguage, + fallbackLng: fallbackLanguage, + interpolation: { + escapeValue: false, + }, +}); + +i18n.on("languageChanged", (language) => { + const normalized = normalizeLanguage(language); + if (normalized) { + localStorage.setItem(LANGUAGE_STORAGE_KEY, normalized); + } +}); + +export default i18n; + diff --git a/src/i18n/languages.ts b/src/i18n/languages.ts new file mode 100644 index 0000000..630ef2b --- /dev/null +++ b/src/i18n/languages.ts @@ -0,0 +1,31 @@ +export const LANGUAGE_STORAGE_KEY = "delta-language"; + +export const supportedLanguages = [ + { code: "en", label: "English", nativeLabel: "English" }, + { code: "zh-CN", label: "Simplified Chinese", nativeLabel: "简体中文" }, +] as const; + +export type SupportedLanguage = (typeof supportedLanguages)[number]["code"]; + +export const fallbackLanguage: SupportedLanguage = "en"; + +export function normalizeLanguage(language?: string): SupportedLanguage | undefined { + if (!language) return undefined; + + const normalized = language.toLowerCase(); + + if ( + normalized === "zh" || + normalized === "zh-cn" || + normalized.startsWith("zh-hans") + ) { + return "zh-CN"; + } + + if (normalized === "en" || normalized.startsWith("en-")) { + return "en"; + } + + return undefined; +} + diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json new file mode 100644 index 0000000..b698bb5 --- /dev/null +++ b/src/i18n/locales/en.json @@ -0,0 +1,111 @@ +{ + "app": { + "name": "Delta", + "logoAlt": "Delta app logo" + }, + "common": { + "cancel": "Cancel", + "close": "Close", + "copy": "Copy", + "delete": "Delete", + "loading": "Loading...", + "notAvailable": "N/A", + "reveal": "Reveal" + }, + "scan": { + "tabs": { + "fullDisk": "Full Disk", + "customPath": "Custom Path" + }, + "title": "Start Snapshot", + "fields": { + "disk": "Disk", + "customPath": "Custom Path" + }, + "placeholders": { + "selectDisk": "Select a disk", + "customPath": "Enter custom path..." + }, + "actions": { + "scan": "Scan", + "browseFolder": "Browse folder" + }, + "options": { + "compareSnapshots": "Compare Snapshots", + "compareSnapshotsDescription": "Compare current scan with previous scanned snapshot", + "saveSnapshot": "Save Snapshot", + "saveSnapshotDescription": "Save current scan into new snapshot file" + }, + "fallback": { + "unknownDisk": "Unknown", + "noDisksFound": "No Disks Found" + } + }, + "snapshot": { + "title": "Local Snapshots", + "description": "Select a snapshot below to remove it from your local storage.", + "empty": "No snapshot files.", + "columns": { + "driveName": "Drive Name", + "date": "Date", + "size": "Size" + }, + "deleteSelected": "Delete Selected", + "deleteTitle": "Delete Snapshot?", + "deleteDescription": "This will permanently delete the selected snapshot from your disk." + }, + "settings": { + "title": "Settings", + "description": "Manage local snapshots and configuration.", + "dataManagement": "Data Management", + "configurations": "Configurations", + "language": "Language", + "languageDescription": "Choose the display language for the app.", + "historyGraph": "History Graph", + "historyGraphDescription": "Enable the historical size graph when viewing directory entries." + }, + "tree": { + "headers": { + "name": "Name", + "size": "Size", + "previous": "Prev", + "change": "Change", + "flag": "Flag" + } + }, + "overview": { + "type": { + "directory": "Directory", + "file": "File" + }, + "metrics": { + "size": "Size", + "previousSize": "Previous Size", + "sizeChange": "Size Change", + "subdirs": "Subdirs", + "previousSubdirs": "Prev Subdirs", + "subdirChange": "Subdir Change", + "files": "Files", + "previousFiles": "Prev Files", + "filesChange": "Files Change" + }, + "dates": { + "created": "Created", + "modified": "Modified" + } + }, + "history": { + "chartLabel": "Size", + "disabled": "History graph disabled.", + "empty": "Select a directory or file to view history." + }, + "notifications": { + "title": "Notifications", + "toggle": "Toggle notifications", + "errorCode": "Error: {{code}}" + }, + "topBar": { + "subdir": "Subdir", + "subfiles": "Subfiles" + } +} diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json new file mode 100644 index 0000000..9934f84 --- /dev/null +++ b/src/i18n/locales/zh-CN.json @@ -0,0 +1,111 @@ +{ + "app": { + "name": "Delta", + "logoAlt": "Delta 应用图标" + }, + "common": { + "cancel": "取消", + "close": "关闭", + "copy": "复制", + "delete": "删除", + "loading": "加载中...", + "notAvailable": "不可用", + "reveal": "显示" + }, + "scan": { + "tabs": { + "fullDisk": "整盘扫描", + "customPath": "自定义路径" + }, + "title": "开始快照", + "fields": { + "disk": "磁盘", + "customPath": "自定义路径" + }, + "placeholders": { + "selectDisk": "选择磁盘", + "customPath": "输入自定义路径..." + }, + "actions": { + "scan": "扫描", + "browseFolder": "选择文件夹" + }, + "options": { + "compareSnapshots": "对比快照", + "compareSnapshotsDescription": "将当前扫描结果与之前保存的快照进行对比", + "saveSnapshot": "保存快照", + "saveSnapshotDescription": "将当前扫描结果保存为新的快照文件" + }, + "fallback": { + "unknownDisk": "未知", + "noDisksFound": "未找到磁盘" + } + }, + "snapshot": { + "title": "本地快照", + "description": "选择下面的快照,将其从本地存储中删除。", + "empty": "没有快照文件。", + "columns": { + "driveName": "磁盘名称", + "date": "日期", + "size": "大小" + }, + "deleteSelected": "删除所选", + "deleteTitle": "删除快照?", + "deleteDescription": "这会从磁盘中永久删除所选快照。" + }, + "settings": { + "title": "设置", + "description": "管理本地快照和应用配置。", + "dataManagement": "数据管理", + "configurations": "配置", + "language": "语言", + "languageDescription": "选择应用界面显示语言。", + "historyGraph": "历史图表", + "historyGraphDescription": "查看目录条目时启用历史大小图表。" + }, + "tree": { + "headers": { + "name": "名称", + "size": "大小", + "previous": "之前", + "change": "变化", + "flag": "标记" + } + }, + "overview": { + "type": { + "directory": "目录", + "file": "文件" + }, + "metrics": { + "size": "大小", + "previousSize": "之前大小", + "sizeChange": "大小变化", + "subdirs": "子目录", + "previousSubdirs": "之前子目录", + "subdirChange": "子目录变化", + "files": "文件", + "previousFiles": "之前文件", + "filesChange": "文件变化" + }, + "dates": { + "created": "创建时间", + "modified": "修改时间" + } + }, + "history": { + "chartLabel": "大小", + "disabled": "历史图表已关闭。", + "empty": "选择一个目录或文件来查看历史记录。" + }, + "notifications": { + "title": "通知", + "toggle": "打开通知", + "errorCode": "错误:{{code}}" + }, + "topBar": { + "subdir": "子目录", + "subfiles": "子文件" + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3a05708..eca5731 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -19,8 +19,8 @@ export function parsePathToSegment(rootPath: string | undefined, fullPath: strin return segments.filter(s => s.length > 0); } -export const formatDateTime = (dateStr: String) => { - return new Date(dateStr.replace(' ', 'T')).toLocaleString('en-US', { +export const formatDateTime = (dateStr: String, locale?: string) => { + return new Date(dateStr.replace(' ', 'T')).toLocaleString(locale, { dateStyle: 'long', timeStyle: 'short', }); @@ -42,4 +42,4 @@ export function appendPaths(prevPath: string, currentPath: string) { } return `${prevPath}${pathSeparator}${currentPath}` -} \ No newline at end of file +} diff --git a/src/main.tsx b/src/main.tsx index ac5ae05..ff25449 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; +import "@/i18n"; import './index.css' ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(