From 4dc634aebbc5aa3dfcfc50ea97dbaff826c015b0 Mon Sep 17 00:00:00 2001 From: Mayne Date: Fri, 25 Oct 2024 11:54:52 +0800 Subject: [PATCH] i18n (#184) * chore: i18n init * chore(i18n): app settings * chore(i18n): sidebar * chore(i18n): nav * chore(i18n): table & doc * chore(i18n): ai chat * fix: nav layout * Update to version 0.8.0 --- apps/desktop/index.tsx | 1 + apps/desktop/settings/storage/page.tsx | 7 +- .../desktop/settings/storage/storage-form.tsx | 20 +- apps/web-app/[database]/[node]/node-cover.tsx | 5 +- apps/web-app/[database]/[node]/node-icon.tsx | 4 +- .../[database]/[node]/node-restore.tsx | 17 +- apps/web-app/[database]/[node]/page.tsx | 8 +- apps/web-app/index.tsx | 1 + apps/web-app/settings/ai/ai-form.tsx | 68 ++-- apps/web-app/settings/ai/local-llm-manage.tsx | 8 +- .../settings/ai/new-llm-provider-form.tsx | 69 ++-- apps/web-app/settings/ai/page.tsx | 10 +- apps/web-app/settings/api/page.tsx | 34 +- .../settings/appearance/appearance-form.tsx | 94 +++-- apps/web-app/settings/appearance/page.tsx | 11 +- apps/web-app/settings/layout.tsx | 54 +-- apps/web-app/settings/page.tsx | 7 +- apps/web-app/settings/profile-form.tsx | 23 +- apps/web-app/settings/storage/page.tsx | 7 +- .../web-app/settings/storage/storage-form.tsx | 66 ++-- components/ai-chat/ai-input-editor/index.tsx | 7 +- components/cmdk/index.tsx | 33 +- components/database-select.tsx | 34 +- components/doc/editor.tsx | 7 +- .../ComponentPickerMenuPlugin/index.tsx | 78 ++--- components/keyboard-shortcuts/const.ts | 330 +++++++++--------- components/keyboard-shortcuts/index.tsx | 19 +- .../keyboard-shortcuts/shortcut-table.tsx | 7 +- components/nav/dropdown-menu.tsx | 56 ++- components/nav/index.tsx | 8 +- components/nav/nav-status.tsx | 12 +- .../nav}/node-update-time.tsx | 7 +- components/nav/update-status.tsx | 8 +- components/node-menu/move-into.tsx | 7 +- components/node-menu/node-export.tsx | 11 +- components/sidebar/everyday.tsx | 14 +- components/sidebar/import-file/index.tsx | 13 +- components/sidebar/index.tsx | 16 +- components/sidebar/trash/index.tsx | 28 +- .../sidebar/tree/create-node-trigger.tsx | 8 +- components/sidebar/tree/node-menu.tsx | 41 ++- components/space-settings/index.tsx | 47 ++- components/table/field-selector.tsx | 14 +- components/table/view-editor/view-editor.tsx | 23 +- components/table/view-field/view-field.tsx | 8 +- .../view-filter-editor/view-filter-editor.tsx | 17 +- .../view-filter-group-editor.tsx | 13 +- components/table/view-item.tsx | 15 +- components/table/view-sort-editor.tsx | 18 +- components/table/view-toolbar.tsx | 4 +- components/table/views/gallery/properties.tsx | 16 +- .../views/grid/fields/field-append-panel.tsx | 37 +- .../table/views/grid/fields/field-delete.tsx | 12 +- .../grid/fields/field-editor-dropdown.tsx | 20 +- .../grid/fields/field-property-editor.tsx | 8 +- .../views/grid/fields/field-type-select.tsx | 101 ++---- lib/env.ts | 2 +- locales/en.json | 321 +++++++++++++++++ locales/i18n.ts | 44 +++ locales/json.d.ts | 4 + locales/zh.json | 322 +++++++++++++++++ package.json | 8 +- pnpm-lock.yaml | 137 ++++++-- 63 files changed, 1643 insertions(+), 806 deletions(-) rename {apps/web-app/[database]/[node] => components/nav}/node-update-time.tsx (69%) create mode 100644 locales/en.json create mode 100644 locales/i18n.ts create mode 100644 locales/json.d.ts create mode 100644 locales/zh.json diff --git a/apps/desktop/index.tsx b/apps/desktop/index.tsx index 55c11922..8e8b6898 100644 --- a/apps/desktop/index.tsx +++ b/apps/desktop/index.tsx @@ -2,6 +2,7 @@ import React from "react" import ReactDOM from "react-dom/client" import { RouterProvider, createBrowserRouter, redirect } from "react-router-dom" +import "@/locales/i18n" import { DownloadPage } from "@/components/landing/download" import SettingsStoragePage from "@/apps/desktop/settings/storage/page" import NodePage from "@/apps/web-app/[database]/[node]/page" diff --git a/apps/desktop/settings/storage/page.tsx b/apps/desktop/settings/storage/page.tsx index 40dada29..d7f90a7c 100644 --- a/apps/desktop/settings/storage/page.tsx +++ b/apps/desktop/settings/storage/page.tsx @@ -1,14 +1,17 @@ import { Separator } from "@/components/ui/separator" +import { useTranslation } from "react-i18next" import { StorageForm } from "./storage-form" export default function SettingsStoragePage() { + const { t } = useTranslation(); + return (
-

Storage

+

{t("settings.storage")}

- Configure your storage settings. + {t("settings.storage.description")}

diff --git a/apps/desktop/settings/storage/storage-form.tsx b/apps/desktop/settings/storage/storage-form.tsx index 5408dc83..9ffe65f7 100644 --- a/apps/desktop/settings/storage/storage-form.tsx +++ b/apps/desktop/settings/storage/storage-form.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import * as z from "zod" +import { useTranslation } from "react-i18next" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -25,6 +26,7 @@ const storageFormSchema = z.object({ type StorageFormValues = z.infer export function StorageForm() { + const { t } = useTranslation() const [dataFolder, setDataFolder] = useState(null) const form = useForm({ @@ -62,8 +64,8 @@ export function StorageForm() { if (!data.dataFolder) { toast({ - title: "Data folder not selected", - description: "You need to select a data folder.", + title: t("settings.storage.dataFolderNotSelected"), + description: t("settings.storage.selectDataFolder"), }) return } @@ -71,7 +73,7 @@ export function StorageForm() { await window.eidos.config.set("dataFolder", data.dataFolder) toast({ - title: "Settings updated", + title: t("settings.storage.settingsUpdated"), }) await window.eidos.reloadApp() } @@ -84,33 +86,33 @@ export function StorageForm() { name="dataFolder" render={({ field }) => ( - Data Folder + {t("settings.storage.dataFolder")}
{dataFolder && ( )}
- The folder where your data will be stored. + {t("settings.storage.dataFolderDescription")}
)} /> diff --git a/apps/web-app/[database]/[node]/node-cover.tsx b/apps/web-app/[database]/[node]/node-cover.tsx index f8628d64..c6c2a6ae 100644 --- a/apps/web-app/[database]/[node]/node-cover.tsx +++ b/apps/web-app/[database]/[node]/node-cover.tsx @@ -1,5 +1,6 @@ import { useRef, useState } from "react" import { useDrop } from "ahooks" +import { useTranslation } from "react-i18next" import { ITreeNode } from "@/lib/store/ITreeNode" import { cn } from "@/lib/utils" @@ -17,6 +18,7 @@ export const NodeCover = (props: { node: ITreeNode }) => { const { node } = props const [open, setOpen] = useState(false) const [isHovering, setIsHovering] = useState(false) + const { t } = useTranslation() const dropRef = useRef(null) @@ -51,7 +53,7 @@ export const NodeCover = (props: { node: ITreeNode }) => {
- +
@@ -75,6 +77,7 @@ export const NodeCover = (props: { node: ITreeNode }) => { {t('doc.coverImage')} { setIcon(props.icon) }, [props.icon]) @@ -94,7 +96,7 @@ export const NodeIconEditor = (props: { size="sm" onClick={handleRemoveIcon} > - Remove + {t("common.remove")}
diff --git a/apps/web-app/[database]/[node]/node-restore.tsx b/apps/web-app/[database]/[node]/node-restore.tsx index 3b789466..dfc98c2c 100644 --- a/apps/web-app/[database]/[node]/node-restore.tsx +++ b/apps/web-app/[database]/[node]/node-restore.tsx @@ -1,4 +1,5 @@ import { useState } from "react" +import { useTranslation } from "react-i18next" import { ITreeNode } from "@/lib/store/ITreeNode" import { useGotoCurrentSpaceHome } from "@/hooks/use-goto" @@ -19,6 +20,7 @@ import { Button } from "@/components/ui/button" export const NodeRestore = ({ node }: { node: ITreeNode | null }) => { const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) const { restoreNode, permanentlyDeleteNode } = useSqlite() + const { t } = useTranslation() const gotoSpaceHome = useGotoCurrentSpaceHome() const confirmDelete = () => { @@ -34,21 +36,21 @@ export const NodeRestore = ({ node }: { node: ITreeNode | null }) => { {Boolean(node && node.is_deleted) && ( <>
- This node is in the trash + {t("doc.nodeInTrash")}
@@ -59,19 +61,18 @@ export const NodeRestore = ({ node }: { node: ITreeNode | null }) => { - Are you absolutely sure? + {t("common.areYouAbsolutelySure")} - This action cannot be undone. This will permanently delete the - node + {t("doc.permanentDeleteWarning")} - Cancel + {t("common.cancel")} - Continue + {t("common.continue")} diff --git a/apps/web-app/[database]/[node]/page.tsx b/apps/web-app/[database]/[node]/page.tsx index d4b16977..b577b9c8 100644 --- a/apps/web-app/[database]/[node]/page.tsx +++ b/apps/web-app/[database]/[node]/page.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react" import { useParams } from "react-router-dom" +import { useTranslation } from "react-i18next" import { DataUpdateSignalType, @@ -32,6 +33,7 @@ export const NodeComponent = ({ nodeId?: string isRootPage?: boolean }) => { + const { t } = useTranslation() const params = useCurrentPathInfo() const { updateNodeName } = useSqlite(params.database) const { tableName } = params @@ -128,19 +130,19 @@ export const NodeComponent = ({ <> {!node.icon && ( )} {!node.cover && ( )} )} {parentNode?.type === "table" && ( )} diff --git a/apps/web-app/index.tsx b/apps/web-app/index.tsx index 71b45338..1e1c3ac6 100644 --- a/apps/web-app/index.tsx +++ b/apps/web-app/index.tsx @@ -1,6 +1,7 @@ import React from "react" import ReactDOM from "react-dom/client" import { RouterProvider, createBrowserRouter, redirect } from "react-router-dom" +import "@/locales/i18n" import { DownloadPage } from "@/components/landing/download" import NodePage from "@/apps/web-app/[database]/[node]/page" diff --git a/apps/web-app/settings/ai/ai-form.tsx b/apps/web-app/settings/ai/ai-form.tsx index db58a095..eb081d40 100644 --- a/apps/web-app/settings/ai/ai-form.tsx +++ b/apps/web-app/settings/ai/ai-form.tsx @@ -1,6 +1,7 @@ import { useEffect } from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" import { isDesktopMode } from "@/lib/env" import { Button } from "@/components/ui/button" @@ -36,6 +37,7 @@ export function AIConfigForm() { }) const { reset } = form const { testModel } = useModelTest() + const { t } = useTranslation() useEffect(() => { reset(aiConfig) @@ -46,7 +48,7 @@ export function AIConfigForm() { setAiConfig(data) // data.token = "sk-**********" toast({ - title: "AI Config updated.", + title: t("settings.ai.configUpdated"), }) } function updateModels(models: string[]) { @@ -72,9 +74,9 @@ export function AIConfigForm() { )} - Provider + {t("settings.ai.provider")} - There are many LLM API providers. configure as your need. + {t("settings.ai.providerDescription")} @@ -94,9 +96,9 @@ export function AIConfigForm() { - Model Preferences + {t("settings.ai.modelPreferences")} - Select preferred models for different tasks + {t("settings.ai.modelPreferencesDescription")} @@ -106,7 +108,9 @@ export function AIConfigForm() { render={({ field }) => (
- Embedding Model + + {t("settings.ai.embeddingModel")} +
- Test + {t("common.test")}
- Select your preferred model for embedding tasks + {t("settings.ai.embeddingModelDescription")}
@@ -139,7 +143,9 @@ export function AIConfigForm() { render={({ field }) => (
- Translation Model + + {t("settings.ai.translationModel")} +
- Test + {t("common.test")}
- Select your preferred model for translation tasks + {t("settings.ai.translationModelDescription")}
@@ -173,7 +179,9 @@ export function AIConfigForm() { render={({ field }) => (
- Coding Model + + {t("settings.ai.codingModel")} +
testModel(TaskType.Coding, field.value)} > - Test + {t("common.test")}
- Select your preferred model for coding tasks + {t("settings.ai.codingModelDescription")}
@@ -201,37 +209,7 @@ export function AIConfigForm() { />
- {/* - - Runtime - - - - ( - - Auto Load Embedding Model - - - - - The embedding model is automatically loaded when the app - starts. It will warm up the embedding model in the worker, - which will make the first search faster. This may increase - memory usage. - - - - )} - /> - - */} - + ) diff --git a/apps/web-app/settings/ai/local-llm-manage.tsx b/apps/web-app/settings/ai/local-llm-manage.tsx index 55447685..ecbb924e 100644 --- a/apps/web-app/settings/ai/local-llm-manage.tsx +++ b/apps/web-app/settings/ai/local-llm-manage.tsx @@ -1,4 +1,5 @@ import { useState } from "react" +import { useTranslation } from "react-i18next" import { downloadWebLLM } from "@/lib/ai/helper" import { cn } from "@/lib/utils" @@ -24,6 +25,7 @@ export const LocalLLMManage = (props: { const [currentModel, setCurrentModel] = useState("") const [progress, setProgress] = useState(0) const [loading, setLoading] = useState(false) + const { t } = useTranslation() const cancelDownload = () => { controller.abort() @@ -51,9 +53,9 @@ export const LocalLLMManage = (props: {
- Local LLM + {t("settings.ai.localLLMTitle")} -

Manage your local LLM.

+

{t("settings.ai.localLLMDescription")}

@@ -65,7 +67,7 @@ export const LocalLLMManage = (props: { excludeLocalModels={models} /> diff --git a/apps/web-app/settings/ai/new-llm-provider-form.tsx b/apps/web-app/settings/ai/new-llm-provider-form.tsx index 81d99ec4..136eccb7 100644 --- a/apps/web-app/settings/ai/new-llm-provider-form.tsx +++ b/apps/web-app/settings/ai/new-llm-provider-form.tsx @@ -1,17 +1,9 @@ +import { useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" import OpenAI from "openai" -import { useState } from "react" import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/react-hook-form/form" import { Button } from "@/components/ui/button" import { Dialog, @@ -30,6 +22,15 @@ import { SelectValue, } from "@/components/ui/select" import { useToast } from "@/components/ui/use-toast" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/react-hook-form/form" import { LLMProvider, llmProviderSchema } from "./store" @@ -45,6 +46,7 @@ export const UpdateLLMProviderForm = ({ children?: React.ReactNode }) => { const [open, setOpen] = useState(false) + const { t } = useTranslation() const handleChange = (data: LLMProvider) => { onChange?.(data) setOpen(false) @@ -54,7 +56,7 @@ export const UpdateLLMProviderForm = ({ {children} - Update LLM Provider + {t("settings.ai.updateLLMProvider")} @@ -63,6 +65,7 @@ export const UpdateLLMProviderForm = ({ } export const NewLLMProviderForm = ({ onAdd }: ILLMProviderManageProps) => { + const { t } = useTranslation() const [open, setOpen] = useState(false) const handleAdd = (data: LLMProvider) => { onAdd(data) @@ -72,14 +75,14 @@ export const NewLLMProviderForm = ({ onAdd }: ILLMProviderManageProps) => { - New LLM Provider + {t("settings.ai.addProvider")} - Add a new LLM provider to the list of providers. + {t("settings.ai.addProviderDescription")} @@ -99,6 +102,7 @@ export const LLMProviderForm = ({ value, onChange, }: LLMProviderFormProps) => { + const { t } = useTranslation() const form = useForm({ resolver: zodResolver(llmProviderSchema), defaultValues: value || { @@ -127,8 +131,8 @@ export const LLMProviderForm = ({ const baseUrl = form.getValues("baseUrl") if (!baseUrl) { toast.toast({ - title: "Error", - description: "Base URL is required to fetch model list.", + title: t("common.error"), + description: t("settings.ai.baseUrlRequired"), }) return } @@ -146,9 +150,8 @@ export const LLMProviderForm = ({ } catch (error) { console.error(error) toast.toast({ - title: "Error", - description: - "Failed to fetch model list! Mostly due to CORS, please check the console for more info. ", + title: t("common.error"), + description: t("settings.ai.fetchModelListError"), }) } } @@ -162,7 +165,7 @@ export const LLMProviderForm = ({ name="name" render={({ field }) => ( - Name + {t("common.name")} ( - Type + {t("settings.ai.providerType")} - This is the base URL used to access the OpenAI API or API - compatible service. + {t("settings.ai.baseUrlDescription")} @@ -224,12 +226,12 @@ export const LLMProviderForm = ({ name="apiKey" render={({ field }) => ( - Api Key + {t("common.apiKey")} - This is the key used to access the API. + {t("settings.ai.apiKeyDescription")} @@ -240,15 +242,10 @@ export const LLMProviderForm = ({ render={({ field }) => (
- Models + {t("settings.ai.models")} {form.watch("type") === "openai" && ( - )}
@@ -259,7 +256,7 @@ export const LLMProviderForm = ({ />
- add models to use, comma separated. + {t("settings.ai.modelsDescription")}
diff --git a/apps/web-app/settings/ai/page.tsx b/apps/web-app/settings/ai/page.tsx index 1770f8b7..39bc3fad 100644 --- a/apps/web-app/settings/ai/page.tsx +++ b/apps/web-app/settings/ai/page.tsx @@ -1,14 +1,16 @@ +import { useTranslation } from "react-i18next" import { Separator } from "@/components/ui/separator" - import { AIConfigForm } from "./ai-form" -export default function SettingsAccountPage() { +export default function SettingsAIPage() { + const { t } = useTranslation() + return (
-

AI Config

+

{t("settings.ai")}

- Configure your AI settings. + {t("settings.ai.description")}

diff --git a/apps/web-app/settings/api/page.tsx b/apps/web-app/settings/api/page.tsx index 09b0a25e..fdca0abc 100644 --- a/apps/web-app/settings/api/page.tsx +++ b/apps/web-app/settings/api/page.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo } from "react" import { zodResolver } from "@hookform/resolvers/zod" import { CopyIcon } from "lucide-react" import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" import { DOMAINS } from "@/lib/const" import { shortenId, uuidv7 } from "@/lib/utils" @@ -24,6 +25,7 @@ import { apiAgentFormSchema, useAPIConfigStore, } from "./store" +import { Separator } from "@/components/ui/separator" // This can come from your database or API. const defaultValues: Partial = { @@ -32,6 +34,7 @@ const defaultValues: Partial = { } export function APIAgentForm() { + const { t } = useTranslation() const { apiAgentConfig, setAPIAgentConfig } = useAPIConfigStore() const form = useForm({ @@ -89,19 +92,21 @@ export function APIAgentForm() { name="url" render={({ field }) => ( - API Agent URL + {t("settings.api.agentUrl")}
- The URL of your API Agent. + + {t("settings.api.agentUrlDescription")} + {Boolean(apiURL.length) && ( - Call API through{" "} + {t("settings.api.callApiThrough")}{" "}
{apiURL} + ) } export default function APISettingsPage() { - return + const { t } = useTranslation() + + return ( +
+
+

{t("settings.api")}

+

+ {t("settings.api.description")} +

+
+ + +
+ ) } diff --git a/apps/web-app/settings/appearance/appearance-form.tsx b/apps/web-app/settings/appearance/appearance-form.tsx index 66afb70c..dbedca6e 100644 --- a/apps/web-app/settings/appearance/appearance-form.tsx +++ b/apps/web-app/settings/appearance/appearance-form.tsx @@ -1,14 +1,17 @@ "use client" +import { useEffect } from "react" +import i18n from "@/locales/i18n" import { zodResolver } from "@hookform/resolvers/zod" import { ChevronDown } from "lucide-react" +import { useTheme } from "next-themes" import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" import * as z from "zod" import { cn } from "@/lib/utils" -import { Button, buttonVariants } from "@/components/ui/button" +import { buttonVariants } from "@/components/ui/button" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" -import { toast } from "@/components/ui/use-toast" import { Form, FormControl, @@ -27,6 +30,10 @@ const appearanceFormSchema = z.object({ invalid_type_error: "Select a font", required_error: "Please select a font.", }), + language: z.enum(["en", "zh"], { + invalid_type_error: "Select a language", + required_error: "Please select a language.", + }), }) type AppearanceFormValues = z.infer @@ -34,34 +41,51 @@ type AppearanceFormValues = z.infer // This can come from your database or API. const defaultValues: Partial = { theme: "light", + language: "en", } export function AppearanceForm() { + const { t } = useTranslation() + const { theme, setTheme } = useTheme() const form = useForm({ resolver: zodResolver(appearanceFormSchema), defaultValues, }) - function onSubmit(data: AppearanceFormValues) { - toast({ - title: "You submitted the following values:", - description: ( -
-          {JSON.stringify(data, null, 2)}
-        
- ), + useEffect(() => { + // Load saved preferences from localStorage + const savedPreferences = localStorage.getItem("appearancePreferences") + if (savedPreferences) { + const parsedPreferences = JSON.parse(savedPreferences) + form.reset(parsedPreferences) + i18n.changeLanguage(parsedPreferences.language) + } + + // Set up event listener for form changes + const subscription = form.watch((data) => { + if (data.theme || data.font || data.language) { + savePreferences(data as AppearanceFormValues) + } }) + + return () => subscription.unsubscribe() + }, [form]) + + function savePreferences(data: AppearanceFormValues) { + localStorage.setItem("appearancePreferences", JSON.stringify(data)) + i18n.changeLanguage(data.language) + setTheme(data.theme) } return (
- - + {/* ( - Font + {t("Font")}
+ + +
+ + {t("Set the font you want to use in the dashboard.")} + + +
+ )} + /> */} + + ( + + {t("settings.appearance.language")} +
+ +
- Set the font you want to use in the dashboard. + {t("settings.appearance.languageDescription")}
)} /> + ( - Theme + {t("settings.appearance.theme")} - Select the theme for the dashboard. + {t("settings.appearance.themeDescription")}
- Light + {t("settings.appearance.light")}
@@ -148,7 +202,7 @@ export function AppearanceForm() {
- Dark + {t("settings.appearance.dark")} @@ -156,8 +210,6 @@ export function AppearanceForm() { )} /> - - ) diff --git a/apps/web-app/settings/appearance/page.tsx b/apps/web-app/settings/appearance/page.tsx index cf9cb05c..47466725 100644 --- a/apps/web-app/settings/appearance/page.tsx +++ b/apps/web-app/settings/appearance/page.tsx @@ -1,14 +1,19 @@ +import { useTranslation } from "react-i18next" + import { Separator } from "@/components/ui/separator" import { AppearanceForm } from "@/apps/web-app/settings/appearance/appearance-form" export default function SettingsAppearancePage() { + const { t } = useTranslation() + return (
-

Appearance

+

+ {t("settings.appearance.title")} +

- Customize the appearance of the app. Automatically switch between day - and night themes. + {t("settings.appearance.description")}

diff --git a/apps/web-app/settings/layout.tsx b/apps/web-app/settings/layout.tsx index 760c4dd2..72936853 100644 --- a/apps/web-app/settings/layout.tsx +++ b/apps/web-app/settings/layout.tsx @@ -2,6 +2,7 @@ import { useKeyPress } from "ahooks" import { Minimize2 } from "lucide-react" +import { useTranslation } from "react-i18next" import { Outlet } from "react-router-dom" import { useGoto } from "@/hooks/use-goto" @@ -14,47 +15,47 @@ import { useLastOpened } from "../[database]/hook" const sidebarNavItems = [ { - title: "General", + titleKey: "settings.general", href: "/settings", }, { - title: "AI", + titleKey: "settings.ai", href: "/settings/ai", }, { - title: "API", + titleKey: "settings.api", href: "/settings/api", }, { - title: "Storage", + titleKey: "settings.storage", href: "/settings/storage", }, - // { - // title: "Backup(deprecated)", - // href: "/settings/backup", - // disabled: true, - // }, { - title: "Sync", + titleKey: "settings.appearance", + href: "/settings/appearance", + }, + { + titleKey: "settings.sync", href: "/settings/sync", disabled: true, }, { - title: "Security", + titleKey: "settings.security", href: "/settings/security", disabled: true, }, - { - title: "Experiment", - href: "/settings/experiment", - }, - { - title: "Devtools", - href: "/settings/dev", - }, + // { + // titleKey: "settings.experiment", + // href: "/settings/experiment", + // }, + // { + // titleKey: "settings.devtools", + // href: "/settings/dev", + // }, ] export default function SettingsLayout() { + const { t } = useTranslation() const { lastOpenedTable, lastOpenedDatabase } = useLastOpened() const goto = useGoto() const goBack = () => goto(lastOpenedDatabase, lastOpenedTable) @@ -70,19 +71,26 @@ export default function SettingsLayout() {
-

Settings

+

+ {t("settings.title")} +

- Manage App Settings and Configuration + {t("settings.manageAppSettings")}

diff --git a/apps/web-app/settings/page.tsx b/apps/web-app/settings/page.tsx index 760eddaa..9a89db2d 100644 --- a/apps/web-app/settings/page.tsx +++ b/apps/web-app/settings/page.tsx @@ -1,13 +1,16 @@ +import { useTranslation } from "react-i18next" import { Separator } from "@/components/ui/separator" import { ProfileForm } from "@/apps/web-app/settings/profile-form" export default function SettingsGeneralPage() { + const { t } = useTranslation() + return (
-

General

+

{t("settings.general")}

- How others will see you when collaborating. + {t("settings.general.description")}

diff --git a/apps/web-app/settings/profile-form.tsx b/apps/web-app/settings/profile-form.tsx index 58ba3691..fb7d9e3c 100644 --- a/apps/web-app/settings/profile-form.tsx +++ b/apps/web-app/settings/profile-form.tsx @@ -2,8 +2,15 @@ import { zodResolver } from "@hookform/resolvers/zod" import { ControllerRenderProps, useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" import * as z from "zod" +import { useActivationCodeStore } from "@/hooks/use-activation" +import { useEidosFileSystemManager } from "@/hooks/use-fs" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { toast } from "@/components/ui/use-toast" import { Form, FormControl, @@ -12,13 +19,7 @@ import { FormLabel, FormMessage, } from "@/components/react-hook-form/form" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { toast } from "@/components/ui/use-toast" -import { useActivationCodeStore } from "@/hooks/use-activation" -import { useEidosFileSystemManager } from "@/hooks/use-fs" import { useConfigStore } from "./store" const profileFormSchema = z.object({ @@ -51,7 +52,7 @@ export function ProfileForm() { }) const { clientId } = useActivationCodeStore() const { efsManager } = useEidosFileSystemManager() - + const { t } = useTranslation() const handleChangeAvatar = async ( field: ControllerRenderProps< { @@ -113,7 +114,7 @@ export function ProfileForm() { name="username" render={({ field }) => ( - Name + {t("common.name")} @@ -126,7 +127,9 @@ export function ProfileForm() { />
- Client ID + + {t("settings.general.clientId")} + @@ -146,7 +149,7 @@ export function ProfileForm() { )} /> - + ) diff --git a/apps/web-app/settings/storage/page.tsx b/apps/web-app/settings/storage/page.tsx index 0020621e..e9003946 100644 --- a/apps/web-app/settings/storage/page.tsx +++ b/apps/web-app/settings/storage/page.tsx @@ -1,13 +1,16 @@ import { Separator } from "@/components/ui/separator" import { StorageForm } from "@/apps/web-app/settings/storage/storage-form" +import { useTranslation } from 'react-i18next' export default function SettingsAccountPage() { + const { t } = useTranslation() + return (
-

Storage

+

{t('settings.storage')}

- Configure your storage settings. + {t('settings.storage.description')}

diff --git a/apps/web-app/settings/storage/storage-form.tsx b/apps/web-app/settings/storage/storage-form.tsx index b6b2450b..f25a99cc 100644 --- a/apps/web-app/settings/storage/storage-form.tsx +++ b/apps/web-app/settings/storage/storage-form.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" import { Check, ChevronsUpDown } from "lucide-react" import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" import * as z from "zod" import { FileSystemType } from "@/lib/storage/eidos-file-system" @@ -35,9 +36,9 @@ import { FormMessage, } from "@/components/react-hook-form/form" -const fsTypes = [ +const getFsTypes = (t: (key: string) => string) => [ { label: "OPFS", value: FileSystemType.OPFS }, - { label: "Native File System", value: FileSystemType.NFS }, + { label: t("settings.storage.nativeFileSystem"), value: FileSystemType.NFS }, ] as const const storageFormSchema = z.object({ @@ -49,6 +50,7 @@ const storageFormSchema = z.object({ type StorageFormValues = z.infer export function StorageForm() { + const { t } = useTranslation() const [localPath, setLocalPath] = useIndexedDB("kv", "localPath", null) const [fsType, setFsType] = useIndexedDB("kv", "fs", FileSystemType.OPFS) @@ -116,15 +118,15 @@ export function StorageForm() { if (data.fsType === FileSystemType.NFS) { if (!localPath) { toast({ - title: "Local path not selected", - description: "You need to select a local path to store your files.", + title: t("settings.storage.dataFolderNotSelected"), + description: t("settings.storage.selectDataFolder"), }) return } if (!isGranted) { toast({ - title: "Permission denied", - description: "You need to grant permission to access the directory.", + title: t("common.error"), + description: t("settings.storage.permissionDenied"), }) return } @@ -135,13 +137,16 @@ export function StorageForm() { setFsType(data.fsType) setAutoBackupGap(data.autoBackupGap) toast({ - title: "Settings updated", + title: t("settings.storage.settingsUpdated"), }) } // get current fsType const currentFsType = form.watch("fsType") + // Use the function to get fsTypes when needed + const fsTypes = getFsTypes(t) + return (
@@ -150,7 +155,7 @@ export function StorageForm() { name="fsType" render={({ field }) => ( - File System + {t("settings.storage.fileSystem")} @@ -165,15 +170,19 @@ export function StorageForm() { {field.value ? fsTypes.find((fsType) => fsType.value === field.value) ?.label - : "Select File System"} + : t("settings.storage.selectFileSystem")} - - No type found. + + + {t("settings.storage.noTypeFound")} + {fsTypes.map((type) => ( - which file system to store your files.
OPFS stores files - in the browser's storage, while Native File System stores files - in a local directory on your device. + {t("settings.storage.fileSystemDescription")}
@@ -215,36 +222,37 @@ export function StorageForm() { name="localPath" render={({ field }) => ( - Local Path + {t("settings.storage.dataFolder")}
- {localPath?.name} + {localPath?.name || + t("settings.storage.dataFolderNotSelected")}
- the local path where your files will be stored. + {t("settings.storage.dataFolderDescription")} {isGranted ? ( {" "} - Permission granted. + {t("settings.storage.permissionGranted")} ) : (
{" "} - Permission denied. + {t("settings.storage.permissionDenied")} {localPath && ( )}
@@ -272,7 +280,7 @@ export function StorageForm() { name="autoBackupGap" render={({ field }) => ( - Auto backup gap(minutes) + {t("settings.storage.autoBackupGap")} {autoBackupGap == 0 - ? "Disable auto save." - : `backup data every ${field.value} minutes, 0 means disable auto - save.`} + ? t("settings.storage.autoBackupDisabled") + : t("settings.storage.autoBackupDescription", { + minutes: field.value, + })}
- backup every space's database to the local path. keep data - more secure. + {t("settings.storage.autoBackupExplanation")}
@@ -302,7 +310,7 @@ export function StorageForm() { )} diff --git a/components/ai-chat/ai-input-editor/index.tsx b/components/ai-chat/ai-input-editor/index.tsx index fc92c555..99550f7b 100644 --- a/components/ai-chat/ai-input-editor/index.tsx +++ b/components/ai-chat/ai-input-editor/index.tsx @@ -17,6 +17,7 @@ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin" import { HeadingNode, QuoteNode } from "@lexical/rich-text" import { Message } from "ai/react" import { $getRoot } from "lexical" +import { useTranslation } from "react-i18next" import { BGEM3 } from "@/lib/ai/llm_vendors/bge" import { embeddingTexts } from "@/lib/embedding/worker" @@ -83,6 +84,7 @@ export const AIInputEditor = ({ setContextNodes, setContextEmbeddings, }: InputEditorProps) => { + const { t } = useTranslation() const initialConfig: InitialConfigType = { namespace: "AI-Chat-Input-Editor", theme, @@ -201,9 +203,10 @@ export const AIInputEditor = ({ } placeholder={
- Type your message here. + {t("aiChat.inputEditor.typeYourMessageHere")}
- Press / to switch prompt. @ to mention resource. + {t("aiChat.inputEditor.pressSlashToSwitchPrompt")} + {t("aiChat.inputEditor.pressAtToMentionResource")}
} ErrorBoundary={LexicalErrorBoundary} diff --git a/components/cmdk/index.tsx b/components/cmdk/index.tsx index 29562786..11b4dfa7 100644 --- a/components/cmdk/index.tsx +++ b/components/cmdk/index.tsx @@ -4,6 +4,7 @@ import { useEffect } from "react" import { useDebounceFn, useKeyPress } from "ahooks" import { Bot, Clock3Icon, FilePlus2Icon, Palette, Settings } from "lucide-react" import { useTheme } from "next-themes" +import { useTranslation } from "react-i18next" import { isInkServiceMode } from "@/lib/env" import { useAppRuntimeStore } from "@/lib/store/runtime-store" @@ -59,7 +60,8 @@ export function CommandDialogDemo() { space && run(input) }, [input, run, space]) - const { isRightPanelOpen: isAiOpen, setIsRightPanelOpen: setIsAiOpen } = useSpaceAppStore() + const { isRightPanelOpen: isAiOpen, setIsRightPanelOpen: setIsAiOpen } = + useSpaceAppStore() const { lastOpenedDatabase } = useLastOpened() const { createDoc } = useSqlite() @@ -91,60 +93,53 @@ export function CommandDialogDemo() { return } + const { t } = useTranslation() + return ( - not found "{input}" + {t("cmdk.notFound", { input })} {!isInkServiceMode && ( - + - Today + {t("common.today")} - {/* - - Everyday - */} - New Draft Doc + {t("cmdk.newDraftDoc")} - AI + {t("common.ai")} - {/* - - Share - */} )} - {/* */} {!isInkServiceMode && ( <> )} - + - Switch Theme + {t("cmdk.switchTheme")} ⌘+Shift+L {!isInkServiceMode && ( - Settings + {t("common.settings")} )} diff --git a/components/database-select.tsx b/components/database-select.tsx index 1bc3b8f7..50b9a4d0 100644 --- a/components/database-select.tsx +++ b/components/database-select.tsx @@ -3,6 +3,7 @@ import * as React from "react" import { kebabCase } from "lodash" import { Check, ChevronsUpDown, PlusCircle } from "lucide-react" +import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils" import { useCurrentPathInfo } from "@/hooks/use-current-pathinfo" @@ -42,6 +43,7 @@ interface IDatabaseSelectorProps { } export function DatabaseSelect({ databases }: IDatabaseSelectorProps) { + const { t } = useTranslation() const [open, setOpen] = React.useState(false) const [file, setFile] = React.useState(null) const { spaceList } = useSpace() @@ -130,7 +132,7 @@ export function DatabaseSelect({ databases }: IDatabaseSelectorProps) { aria-expanded={open} className="w-full min-w-[180px] justify-between" > - {space ?
{space}
: "Select Database..."} + {space ?
{space}
: t('space.select.selectDatabase')} @@ -138,12 +140,12 @@ export function DatabaseSelect({ databases }: IDatabaseSelectorProps) { -
No database found.
+
{t('common.noResultsFound')}
{databases.map((database) => ( @@ -171,8 +173,8 @@ export function DatabaseSelect({ databases }: IDatabaseSelectorProps) { setShowNewTeamDialog(true) }} > - {" "} - Create New + + {t('space.select.createNew')} @@ -182,25 +184,24 @@ export function DatabaseSelect({ databases }: IDatabaseSelectorProps) { - Create Space + {t('space.select.createSpace')} - Add a new space to manage data for you + {t('space.select.createSpaceDescription')}
- + { - // disable non-ascii characters, sqlite-wasm handle non-ascii characters incorrectly if (e.target.value) { e.target.validity.valid && setDatabaseName(e.target.value) } else { @@ -212,7 +213,7 @@ export function DatabaseSelect({ databases }: IDatabaseSelectorProps) { {isExistingSpace && !isOverwrite && ( - this space already exists, choose another name + {t('space.select.spaceAlreadyExists')} )} @@ -220,9 +221,9 @@ export function DatabaseSelect({ databases }: IDatabaseSelectorProps) {
- +
- if you export space as a zip file, you can import it here + {t('space.select.importFromFileDescription')}
{isOverwrite && ( - it seems you are trying to overwrite an existing space. please - be careful, this will overwrite data in the existing space. + {t('space.select.overwriteWarning')} )}
@@ -242,14 +242,14 @@ export function DatabaseSelect({ databases }: IDatabaseSelectorProps) {
diff --git a/components/doc/editor.tsx b/components/doc/editor.tsx index e54519f8..d10dbe9a 100644 --- a/components/doc/editor.tsx +++ b/components/doc/editor.tsx @@ -8,6 +8,7 @@ import { ContentEditable } from "@lexical/react/LexicalContentEditable" import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary" import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin" import { useDebounceFn } from "ahooks" +import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils" import { AIEditorPlugin } from "@/components/doc/plugins/AIEditorPlugin" @@ -50,6 +51,7 @@ interface EditorProps { } export function InnerEditor(props: EditorProps) { + const { t } = useTranslation() const ref = React.useRef(null) const { isToolbarVisible, isAIToolsOpen } = useEditorStore() const [floatingAnchorElem, setFloatingAnchorElem] = @@ -102,7 +104,7 @@ export function InnerEditor(props: EditorProps) { } placeholder={
- {props.placeholder ?? "press / for Command"} + {props.placeholder ?? t('doc.pressForCommand')}
} ErrorBoundary={LexicalErrorBoundary} @@ -144,6 +146,7 @@ export function InnerEditor(props: EditorProps) { } export function Editor(props: EditorProps) { + const { t } = useTranslation() const canChangeTitle = props.onTitleChange !== undefined const [title, setTitle] = useState(props.title ?? "") const isLoading = useLoadingExtBlocks() @@ -186,7 +189,7 @@ export function Editor(props: EditorProps) { {props.beforeTitle &&
{props.beforeTitle}
} (null) @@ -183,7 +185,7 @@ export function ComponentPickerMenuPlugin(): JSX.Element { .map((n: string) => parseInt(n, 10)) options.push( - new ComponentPickerOption(`${rows}x${columns} Table`, { + new ComponentPickerOption(t("doc.menu.insertTable", { rows, columns }), { icon: , keywords: ["table"], onSelect: () => @@ -197,7 +199,7 @@ export function ComponentPickerMenuPlugin(): JSX.Element { options.push( ...Array.from({ length: 5 }, (_, i) => i + 1).map( (columns) => - new ComponentPickerOption(`${rows}x${columns} Table`, { + new ComponentPickerOption(t("doc.menu.insertTable", { rows, columns }), { icon: , keywords: ["table"], onSelect: () => @@ -209,16 +211,11 @@ export function ComponentPickerMenuPlugin(): JSX.Element { } return options - }, [editor, queryString]) + }, [editor, queryString, t]) const options = useMemo(() => { const baseOptions = [ - // new ComponentPickerOption("AI Complete", { - // icon: IconMap["ai"], - // keywords: ["ai", "auto"], - // onSelect: () => editor.dispatchCommand(AI_COMPLETE_COMMAND, ""), - // }), - new ComponentPickerOption("Paragraph", { + new ComponentPickerOption(t("doc.menu.paragraph"), { icon: IconMap["text"], keywords: ["normal", "paragraph", "p", "text"], onSelect: () => @@ -231,7 +228,7 @@ export function ComponentPickerMenuPlugin(): JSX.Element { }), ...Array.from({ length: 3 }, (_, i) => i + 1).map( (n) => - new ComponentPickerOption(`Heading ${n}`, { + new ComponentPickerOption(t("doc.menu.heading", { n }), { icon: IconMap[`h${n}`], keywords: ["heading", "header", `h${n}`], onSelect: () => @@ -246,25 +243,25 @@ export function ComponentPickerMenuPlugin(): JSX.Element { }), }) ), - new ComponentPickerOption("Numbered List", { + new ComponentPickerOption(t("doc.menu.numberedList"), { icon: IconMap["lo"], keywords: ["numbered list", "ordered list", "ol"], onSelect: () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined), }), - new ComponentPickerOption("Bulleted List", { + new ComponentPickerOption(t("doc.menu.bulletedList"), { icon: IconMap["ul"], keywords: ["bulleted list", "unordered list", "ul"], onSelect: () => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined), }), - new ComponentPickerOption("Check List", { + new ComponentPickerOption(t("doc.menu.checkList"), { icon: IconMap["cl"], keywords: ["check list", "todo list"], onSelect: () => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined), }), - new ComponentPickerOption("Quote", { + new ComponentPickerOption(t("doc.menu.quote"), { icon: IconMap["quote"], keywords: ["block quote"], onSelect: () => @@ -275,7 +272,7 @@ export function ComponentPickerMenuPlugin(): JSX.Element { } }), }), - new ComponentPickerOption("Code", { + new ComponentPickerOption(t("doc.menu.code"), { icon: IconMap["code"], keywords: ["javascript", "python", "js", "codeblock"], onSelect: () => @@ -286,7 +283,6 @@ export function ComponentPickerMenuPlugin(): JSX.Element { if (selection.isCollapsed()) { $setBlocksType(selection, () => $createCodeNode()) } else { - // Will this ever happen? const textContent = selection.getTextContent() const codeNode = $createCodeNode() selection.insertNodes([codeNode]) @@ -295,14 +291,13 @@ export function ComponentPickerMenuPlugin(): JSX.Element { } }), }), - new ComponentPickerOption("Divider", { + new ComponentPickerOption(t("doc.menu.divider"), { icon: IconMap["hr"], keywords: ["horizontal rule", "divider", "hr"], onSelect: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined), }), - - new ComponentPickerOption("Image", { + new ComponentPickerOption(t("doc.menu.image"), { icon: IconMap["image"], keywords: ["image", "img"], onSelect: () => @@ -311,7 +306,6 @@ export function ComponentPickerMenuPlugin(): JSX.Element { altText: "", }), }), - ...BuiltInBlocks.map((block) => { const iconName = block.icon const BlockIcon = (icons as any)[iconName] @@ -321,8 +315,7 @@ export function ComponentPickerMenuPlugin(): JSX.Element { onSelect: () => block.onSelect(editor), }) }), - - new ComponentPickerOption("Bookmark", { + new ComponentPickerOption(t("doc.menu.bookmark"), { icon: IconMap["bookmark"], keywords: ["bookmark"], onSelect: () => @@ -330,30 +323,27 @@ export function ComponentPickerMenuPlugin(): JSX.Element { url: "", }), }), - - new ComponentPickerOption("Table Of Content", { + new ComponentPickerOption(t("doc.menu.tableOfContent"), { icon: , keywords: ["table of content", "toc"], onSelect: () => editor.dispatchCommand(INSERT_TOC_COMMAND, undefined), }), - - new ComponentPickerOption("Query", { + new ComponentPickerOption(t("doc.menu.query"), { icon: IconMap["sql"], keywords: ["query", "sql"], onSelect: () => - showModal("Insert SqlQuery", (onClose) => ( + showModal(t("doc.menu.insertSqlQuery"), (onClose) => ( )), }), - - new ComponentPickerOption("DatabaseTable", { + new ComponentPickerOption(t("doc.menu.databaseTable"), { icon: IconMap["database"], keywords: ["database", "table"], disabled: true, onSelect: () => { // disable for now return - showModal("Insert Database Table", (onClose) => ( + showModal(t("doc.menu.insertDatabaseTable"), (onClose) => ( - // new ComponentPickerOption(`Align ${alignment}`, { - // icon: , - // keywords: ["align", "justify", alignment], - // onSelect: () => - // // @ts-ignore Correct types, but since they're dynamic TS doesn't like it. - // editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment), - // }) - // ), ...bgColors.map(({ name, value }) => { - return new ComponentPickerOption(`Background ${name}`, { + return new ComponentPickerOption(t("doc.menu.background", { name }), { icon: ( { - return new ComponentPickerOption(`Color ${name}`, { + return new ComponentPickerOption(t("doc.menu.color", { name }), { icon: , keywords: ["color", name], onSelect: () => @@ -426,17 +405,6 @@ export function ComponentPickerMenuPlugin(): JSX.Element { onSelect: () => block.onSelect(editor), }) }), - - // ...["left", "center", "right", "justify"].map( - // (alignment) => - // new ComponentPickerOption(`Align ${alignment}`, { - // icon: , - // keywords: ["align", "justify", alignment], - // onSelect: () => - // // @ts-ignore Correct types, but since they're dynamic TS doesn't like it. - // editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment), - // }) - // ), ] const dynamicOptions = getDynamicOptions() @@ -454,7 +422,7 @@ export function ComponentPickerMenuPlugin(): JSX.Element { }), ] : baseOptions - }, [editor, extBlocks, getDynamicOptions, queryString, showModal]) + }, [editor, extBlocks, getDynamicOptions, queryString, showModal, t]) const onSelectOption = useCallback( ( diff --git a/components/keyboard-shortcuts/const.ts b/components/keyboard-shortcuts/const.ts index 2fddf5e7..f52f252c 100644 --- a/components/keyboard-shortcuts/const.ts +++ b/components/keyboard-shortcuts/const.ts @@ -1,166 +1,168 @@ -export const TableKeyboardShortcuts = [ - { - key: "Arrow", - description: - "Moves the currently selected cell and clears other selections", - }, - { - key: "Shift + Arrow", - description: - "Extends the current selection range in the direction pressed.", - }, - { - key: "Alt + Arrow", - description: - "Moves the currently selected cell and retains the current selection", - }, - { - key: "Ctrl/Cmd + Arrow | Home/End", - description: - "Move the selection as far as possible in the direction pressed.", - }, - { - key: "Ctrl/Cmd + Shift + Arrow", - description: - "Extends the selection as far as possible in the direction pressed.", - }, - { - key: "Shift + Home/End", - description: - "Extends the selection as far as possible in the direction pressed.", - }, - { - key: "Ctrl/Cmd + A", - description: "Selects all cells.", - }, - { - key: "Shift + Space", - description: "Selecs the current row.", - }, - { - key: "Ctrl + Space", - description: "Selects the current col.", - }, - { - key: "PageUp/PageDown", - description: "Moves the current selection up/down by one page.", - }, - { - key: "Escape", - description: "Clear the current selection.", - }, - { - key: "Ctrl/Cmd + D", - description: - "Data from the first row of the range will be down filled into the rows below it", - flag: "downFill", - }, - { - key: "Ctrl/Cmd + R", - description: - "Data from the first column of the range will be right filled into the columns next to it", - flag: "rightFill", - }, - { - key: "Ctrl/Cmd + C", - description: "Copies the current selection.", - }, - { - key: "Ctrl/Cmd + V", - description: "Pastes the current buffer into the grid.", - }, - { - key: "Ctrl/Cmd + F", - description: "Opens the search interface.(disabled for now)", - flag: "search", - disabled: true, - }, - { - key: "Ctrl/Cmd + Home/End", - description: "Move the selection to the first/last cell in the data grid.", - flag: "first/last", - }, - { - key: "Ctrl/Cmd + Shift + Home/End", - description: - "Extend the selection to the first/last cell in the data grid.", - flag: "first/last", - }, -] +import { useTranslation } from 'react-i18next'; -/** - * support most markdown syntax - */ -export const DocumentKeyboardShortcuts = [ - { - key: "Ctrl/Cmd + B", - description: "Bold text", - }, - { - key: "Ctrl/Cmd + I", - description: "Italicize text", - }, - { - key: "Ctrl/Cmd + U", - description: "Underline text", - }, - { - key: "Ctrl/Cmd + S", - description: "Save the document", - }, - { - key: "#", - description: "Heading 1", - }, - { - key: "##", - description: "Heading 2", - }, - { - key: "###", - description: "Heading 3", - }, - { - key: "[]", - description: "Checkbox", - }, - { - key: "-", - description: "Unordered List", - }, - { - key: "number + .", - description: "Ordered List", - }, - { - key: "```", - description: "Code Block", - }, - { - key: "---", - description: "Horizontal Rule", - }, -] +export const useTableKeyboardShortcuts = () => { + const { t } = useTranslation(); -export const CommonKeyboardShortcuts = [ - { - key: "Ctrl/Cmd + /", - description: "Toggle chatbot", - }, - { - key: "Ctrl/Cmd + \\", - description: "Toggle sidebar", - }, - { - key: "Ctrl/Cmd + Shift + L", - description: "Toggle light/dark mode", - }, - { - key: "Ctrl/Cmd + K", - description: "Toggle command palette", - }, - { - key: "Ctrl/Cmd + ,", - description: "Open settings", - }, -] + return [ + { + key: "Arrow", + description: t('kbd.shortcuts.table.arrowDescription'), + }, + { + key: "Shift + Arrow", + description: t('kbd.shortcuts.table.shiftArrowDescription'), + }, + { + key: "Alt + Arrow", + description: t('kbd.shortcuts.table.altArrowDescription'), + }, + { + key: "Ctrl/Cmd + Arrow | Home/End", + description: t('kbd.shortcuts.table.ctrlArrowDescription'), + }, + { + key: "Ctrl/Cmd + Shift + Arrow", + description: t('kbd.shortcuts.table.ctrlShiftArrowDescription'), + }, + { + key: "Shift + Home/End", + description: t('kbd.shortcuts.table.shiftHomeEndDescription'), + }, + { + key: "Ctrl/Cmd + A", + description: t('kbd.shortcuts.table.ctrlADescription'), + }, + { + key: "Shift + Space", + description: t('kbd.shortcuts.table.shiftSpaceDescription'), + }, + { + key: "Ctrl + Space", + description: t('kbd.shortcuts.table.ctrlSpaceDescription'), + }, + { + key: "PageUp/PageDown", + description: t('kbd.shortcuts.table.pageUpDownDescription'), + }, + { + key: "Escape", + description: t('kbd.shortcuts.table.escapeDescription'), + }, + { + key: "Ctrl/Cmd + D", + description: t('kbd.shortcuts.table.ctrlDDescription'), + flag: "downFill", + }, + { + key: "Ctrl/Cmd + R", + description: t('kbd.shortcuts.table.ctrlRDescription'), + flag: "rightFill", + }, + { + key: "Ctrl/Cmd + C", + description: t('kbd.shortcuts.table.ctrlCDescription'), + }, + { + key: "Ctrl/Cmd + V", + description: t('kbd.shortcuts.table.ctrlVDescription'), + }, + { + key: "Ctrl/Cmd + F", + description: t('kbd.shortcuts.table.ctrlFDescription'), + flag: "search", + disabled: true, + }, + { + key: "Ctrl/Cmd + Home/End", + description: t('kbd.shortcuts.table.ctrlHomeEndDescription'), + flag: "first/last", + }, + { + key: "Ctrl/Cmd + Shift + Home/End", + description: t('kbd.shortcuts.table.ctrlShiftHomeEndDescription'), + flag: "first/last", + }, + ]; +}; + +export const useDocumentKeyboardShortcuts = () => { + const { t } = useTranslation(); + + return [ + { + key: "Ctrl/Cmd + B", + description: t('kbd.shortcuts.document.ctrlBDescription'), + }, + { + key: "Ctrl/Cmd + I", + description: t('kbd.shortcuts.document.ctrlIDescription'), + }, + { + key: "Ctrl/Cmd + U", + description: t('kbd.shortcuts.document.ctrlUDescription'), + }, + { + key: "Ctrl/Cmd + S", + description: t('kbd.shortcuts.document.ctrlSDescription'), + }, + { + key: "#", + description: t('kbd.shortcuts.document.heading1Description'), + }, + { + key: "##", + description: t('kbd.shortcuts.document.heading2Description'), + }, + { + key: "###", + description: t('kbd.shortcuts.document.heading3Description'), + }, + { + key: "[]", + description: t('kbd.shortcuts.document.checkboxDescription'), + }, + { + key: "-", + description: t('kbd.shortcuts.document.unorderedListDescription'), + }, + { + key: "number + .", + description: t('kbd.shortcuts.document.orderedListDescription'), + }, + { + key: "```", + description: t('kbd.shortcuts.document.codeBlockDescription'), + }, + { + key: "---", + description: t('kbd.shortcuts.document.horizontalRuleDescription'), + }, + ]; +}; + +export const useCommonKeyboardShortcuts = () => { + const { t } = useTranslation(); + + return [ + { + key: "Ctrl/Cmd + /", + description: t('kbd.shortcuts.common.toggleChatbotDescription'), + }, + { + key: "Ctrl/Cmd + \\", + description: t('kbd.shortcuts.common.toggleSidebarDescription'), + }, + { + key: "Ctrl/Cmd + Shift + L", + description: t('kbd.shortcuts.common.toggleThemeDescription'), + }, + { + key: "Ctrl/Cmd + K", + description: t('kbd.shortcuts.common.toggleCommandPaletteDescription'), + }, + { + key: "Ctrl/Cmd + ,", + description: t('kbd.shortcuts.common.openSettingsDescription'), + }, + ]; +}; diff --git a/components/keyboard-shortcuts/index.tsx b/components/keyboard-shortcuts/index.tsx index 4b51d622..be9d0a55 100644 --- a/components/keyboard-shortcuts/index.tsx +++ b/components/keyboard-shortcuts/index.tsx @@ -1,16 +1,23 @@ +import { useTranslation } from "react-i18next" + import { useAppRuntimeStore } from "@/lib/store/runtime-store" import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog" import { - CommonKeyboardShortcuts, - DocumentKeyboardShortcuts, - TableKeyboardShortcuts, + useCommonKeyboardShortcuts, + useDocumentKeyboardShortcuts, + useTableKeyboardShortcuts, } from "./const" import { ShortcutTable } from "./shortcut-table" export function KeyboardShortCuts() { const { isKeyboardShortcutsOpen, setKeyboardShortcutsOpen } = useAppRuntimeStore() + const { t } = useTranslation() + const CommonKeyboardShortcuts = useCommonKeyboardShortcuts() + const DocumentKeyboardShortcuts = useDocumentKeyboardShortcuts() + const TableKeyboardShortcuts = useTableKeyboardShortcuts() + return (
diff --git a/components/keyboard-shortcuts/shortcut-table.tsx b/components/keyboard-shortcuts/shortcut-table.tsx index 3e062f19..7cb817a4 100644 --- a/components/keyboard-shortcuts/shortcut-table.tsx +++ b/components/keyboard-shortcuts/shortcut-table.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from "react-i18next" + import { Table, TableBody, @@ -13,14 +15,15 @@ interface ShortcutTableProps { title?: string } export const ShortcutTable = ({ shortcuts, title }: ShortcutTableProps) => { + const { t } = useTranslation() return (

{title || "Keyboard Shortcuts"}

- Shortcut - Description + {t("kbd.shortcuts.common.shortcut")} + {t("kbd.shortcuts.common.description")} diff --git a/components/nav/dropdown-menu.tsx b/components/nav/dropdown-menu.tsx index 64ce3fff..fd8792be 100644 --- a/components/nav/dropdown-menu.tsx +++ b/components/nav/dropdown-menu.tsx @@ -13,6 +13,7 @@ import { ScanTextIcon, Trash2Icon, } from "lucide-react" +import { useTranslation } from "react-i18next" import { Link, useNavigate } from "react-router-dom" import { BGEM3 } from "@/lib/ai/llm_vendors/bge" @@ -48,7 +49,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { DiscordIcon } from "@/components/icons/discord" -import { NodeUpdateTime } from "@/apps/web-app/[database]/[node]/node-update-time" +import { NodeUpdateTime } from "@/components/nav/node-update-time" import { useExperimentConfigStore } from "@/apps/web-app/settings/experiment/store" import { CopyShowHide } from "../copy-show-hide" @@ -60,6 +61,7 @@ import { VCardQrCode } from "../vcard-qr-code" import { UpdateStatusComponent } from "./update-status" export function NavDropdownMenu() { + const { t } = useTranslation() const router = useNavigate() const [open, setOpen] = useState(false) const { hasEmbeddingModel, embeddingTexts } = useEmbedding() @@ -98,7 +100,7 @@ export function NavDropdownMenu() { const handleCreateDocEmbedding = async () => { if (node) { toast({ - title: `Creating Embedding for ${node.name}`, + title: t("nav.dropdown.menu.creatingEmbedding", { name: node.name }), }) await createEmbedding({ id: node.id, @@ -107,7 +109,7 @@ export function NavDropdownMenu() { provider: new BGEM3(embeddingTexts), }) toast({ - title: "Embedding Created", + title: t("nav.dropdown.menu.embeddingCreated"), }) } } @@ -117,12 +119,12 @@ export function NavDropdownMenu() { - Send mail to Eidos + {t("nav.dropdown.menu.sendMailToEidos")} {node && (
@@ -131,10 +133,7 @@ export function NavDropdownMenu() { {node && }

- 1. Scan the QR code to add the address to your contacts -
- 2. Send an email to this address to save data into this table -
+ {t("nav.dropdown.menu.emailInstructions")}

)} @@ -148,21 +147,19 @@ export function NavDropdownMenu() { - {/* All data hosted on Local 🖥 */} - {/* */} - Command Palette + {t("nav.dropdown.menu.commandPalette")} ⌘K - Keyboard Shortcuts + {t("nav.dropdown.menu.keyboardShortcuts")} - Settings + {t("common.settings")} @@ -178,17 +175,10 @@ export function NavDropdownMenu() { Discord - {/* - - - Wiki - - */} - - Website + {t("nav.dropdown.menu.website")} @@ -204,7 +194,7 @@ export function NavDropdownMenu() { toggleNodeFullWidth(node) }} > - Full Width + {t("nav.dropdown.menu.fullWidth")} - Lock + {t("nav.dropdown.menu.lock")} @@ -225,20 +215,19 @@ export function NavDropdownMenu() { - Mail + {t("nav.dropdown.menu.mail")} )} - {/* node related operate */} {node.type === "doc" && ( <> - Move Into + {t("node.menu.moveInto")} @@ -251,7 +240,7 @@ export function NavDropdownMenu() {
- Embedding(Beta) + {t("nav.dropdown.menu.embedding")}
@@ -261,7 +250,7 @@ export function NavDropdownMenu() { )} - Delete + {t("common.delete")} @@ -270,12 +259,17 @@ export function NavDropdownMenu() { - Download + {t("common.download")} - Version: {EIDOS_VERSION} ({isDesktopMode ? "Desktop" : "Web"}) + {t("nav.dropdown.menu.version", { + version: EIDOS_VERSION, + mode: isDesktopMode + ? t("nav.dropdown.menu.desktop") + : t("nav.dropdown.menu.web"), + })}
diff --git a/components/nav/index.tsx b/components/nav/index.tsx index f117286e..d00359c6 100644 --- a/components/nav/index.tsx +++ b/components/nav/index.tsx @@ -1,13 +1,13 @@ import { Menu, PanelRightIcon } from "lucide-react" import { useTheme } from "next-themes" +import { useSpaceAppStore } from "@/apps/web-app/[database]/store" +import { Button } from "@/components/ui/button" +import { useSidebar } from "@/components/ui/sidebar" import { isDesktopMode } from "@/lib/env" import { useAppStore } from "@/lib/store/app-store" import { cn } from "@/lib/utils" import { isMac } from "@/lib/web/helper" -import { Button } from "@/components/ui/button" -import { useSidebar } from "@/components/ui/sidebar" -import { useSpaceAppStore } from "@/apps/web-app/[database]/store" import { BreadCrumb } from "./breadcrumb" import { NavDropdownMenu } from "./dropdown-menu" @@ -43,7 +43,7 @@ export const Nav = ({ showMenu = true }: { showMenu?: boolean }) => { "flex h-8 w-full border-separate items-center justify-between pl-2 shrink-0", { fixed: navigator.windowControlsOverlay?.visible, - "!ml-[72px]": + "!pl-[72px]": (isDesktopMode || navigator.windowControlsOverlay?.visible) && isMac() && !isSidebarOpen, diff --git a/components/nav/nav-status.tsx b/components/nav/nav-status.tsx index be2662db..31ed21d9 100644 --- a/components/nav/nav-status.tsx +++ b/components/nav/nav-status.tsx @@ -8,6 +8,7 @@ import { PinOffIcon, Unplug } from "lucide-react" +import { useTranslation } from "react-i18next" import { AvatarList } from "@/components/avatar-list" import { Button } from "@/components/ui/button" @@ -53,6 +54,7 @@ const AppInfoMap: Record< } export const NavStatus = () => { + const { t } = useTranslation() const { isRightPanelOpen, setIsRightPanelOpen, @@ -84,16 +86,16 @@ export const NavStatus = () => { )} {!isDesktopMode && (
{connected ? ( @@ -130,8 +132,8 @@ export const NavStatus = () => {

{currentNode?.is_pinned - ? "Click to unpin this node" - : "Click to pin this node"} + ? t('nav.status.clickToUnpin') + : t('nav.status.clickToPin')}

diff --git a/apps/web-app/[database]/[node]/node-update-time.tsx b/components/nav/node-update-time.tsx similarity index 69% rename from apps/web-app/[database]/[node]/node-update-time.tsx rename to components/nav/node-update-time.tsx index 58f301ce..6a3bea17 100644 --- a/apps/web-app/[database]/[node]/node-update-time.tsx +++ b/components/nav/node-update-time.tsx @@ -1,12 +1,17 @@ +import { useTranslation } from "react-i18next" + import { timeAgo } from "@/lib/utils" import { useCurrentNode } from "@/hooks/use-current-node" import { useNodeBaseInfo } from "@/hooks/use-node-base-info" export const NodeUpdateTime = () => { const node = useCurrentNode() + const { t } = useTranslation() const { updated_at } = useNodeBaseInfo(node) const tips = updated_at - ? "last updated: " + timeAgo(new Date(updated_at + "Z")) + ? t("nav.dropdown.menu.lastUpdated", { + time: timeAgo(new Date(updated_at + "Z")), + }) : "" if (!updated_at?.length) return null return
{tips}
diff --git a/components/nav/update-status.tsx b/components/nav/update-status.tsx index b38313ce..8712f146 100644 --- a/components/nav/update-status.tsx +++ b/components/nav/update-status.tsx @@ -1,11 +1,13 @@ import { useEffect } from "react" import { Download, RefreshCw } from "lucide-react" +import { useTranslation } from "react-i18next" import { isDesktopMode } from "@/lib/env" import { useUpdateStatus } from "@/hooks/use-update-status" import { DropdownMenuItem } from "@/components/ui/dropdown-menu" export function UpdateStatusComponent() { + const { t } = useTranslation() const { updateStatus, updateInfo, checkForUpdates, quitAndInstall } = useUpdateStatus() @@ -30,17 +32,17 @@ export function UpdateStatusComponent() { {updateStatus === "available" && ( - Update to v{updateInfo?.version} + {t('nav.status.updateAvailable', { version: updateInfo?.version })} )} {updateStatus === "not-available" && ( - No updates available + {t('nav.status.noUpdatesAvailable')} )} - Check for updates + {t('nav.status.checkForUpdates')} ) diff --git a/components/node-menu/move-into.tsx b/components/node-menu/move-into.tsx index b6334675..8852097f 100644 --- a/components/node-menu/move-into.tsx +++ b/components/node-menu/move-into.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from "react-i18next" + import { ITreeNode } from "@/lib/store/ITreeNode" import { useNodeTree } from "@/hooks/use-node-tree" import { useAllNodes } from "@/hooks/use-nodes" @@ -19,6 +21,7 @@ export const NodeMoveInto = ({ node }: { node: ITreeNode }) => { type: ["table", "folder"], isDeleted: false, }) + const { t } = useTranslation() const { sqlite } = useSqlite() const { setNode } = useNodeTree() @@ -32,10 +35,10 @@ export const NodeMoveInto = ({ node }: { node: ITreeNode }) => { } return ( - + - No table found. + {t("common.noTableFound")} {tableNodes.map((tableNode, index) => ( { const { sqlite } = useSqlite() + const { t } = useTranslation() const exportDoc = async (docId: string) => { const file = await sqlite?.exportMarkdown(docId) @@ -42,7 +44,7 @@ export const NodeExportContextMenu = ({ node }: { node: ITreeNode }) => { - Export{" "} + {t("common.export")} { - Export + {t("common.export")} @@ -80,6 +82,7 @@ export const NodeExportContextMenu = ({ node }: { node: ITreeNode }) => { export const NodeExport = ({ node }: { node: ITreeNode }) => { const { sqlite } = useSqlite() + const { t } = useTranslation() const exportDoc = async (docId: string) => { const md = await sqlite?.exportMarkdown(docId) @@ -104,7 +107,7 @@ export const NodeExport = ({ node }: { node: ITreeNode }) => { - Export + {t("common.export")} { - Export + {t("common.export")} { const { sqlite } = useSqlite(space) + const { t } = useTranslation() const { convertMarkdown2State } = useDocEditor(sqlite) const [progress, setProgress] = useState(0) const [importing, setImporting] = useState(false) @@ -82,26 +84,26 @@ export const EverydaySidebarItem = ({ space }: { space: string }) => { > - Today + {t("common.today")} - Open - Download + {t("common.open")} + {t("common.download")} - Import + {t("common.import")} - Import From Logseq(Beta) + {t("everyday.import.title")}
-
click here to select logseq journal folder
+
{t("everyday.import.selectFolder")}
{importing && } diff --git a/components/sidebar/import-file/index.tsx b/components/sidebar/import-file/index.tsx index 5935dd0d..10892371 100644 --- a/components/sidebar/import-file/index.tsx +++ b/components/sidebar/import-file/index.tsx @@ -1,5 +1,6 @@ import { useState } from "react" import { Plus } from "lucide-react" +import { useTranslation } from "react-i18next" import { Button } from "@/components/ui/button" import { @@ -16,7 +17,7 @@ import { ImportTable } from "./import-table" export function ImportFileDialog() { const [open, setOpen] = useState(false) - + const { t } = useTranslation() return ( @@ -28,20 +29,22 @@ export function ImportFileDialog() { > - Import + {t("common.import")} - Import File + {t("sidebar.importFile.title")} - Import a CSV file to create a new table
Import a markdown - file to create a new document + {t("sidebar.importFile.importCSVDescription")}
+ {t("sidebar.importFile.importMarkdownDescription")}
+ +
diff --git a/components/sidebar/index.tsx b/components/sidebar/index.tsx index 061dd1b9..37eac257 100644 --- a/components/sidebar/index.tsx +++ b/components/sidebar/index.tsx @@ -10,6 +10,7 @@ import { PinIcon, } from "lucide-react" import { Link } from "react-router-dom" +import { useTranslation } from "react-i18next" import { isDesktopMode } from "@/lib/env" import { useAppStore } from "@/lib/store/app-store" @@ -44,6 +45,7 @@ import { useTreeOperations } from "./tree/hooks" import { useFolderStore } from "./tree/store" export const SideBar = ({ className }: any) => { + const { t } = useTranslation() const { space } = useCurrentPathInfo() const [loading, setLoading] = useState(true) const { updateNodeList } = useSqlite(space) @@ -86,7 +88,7 @@ export const SideBar = ({ className }: any) => {
{isShareMode ? ( - "ShareMode" + t("common.shareMode") ) : ( <> @@ -109,7 +111,7 @@ export const SideBar = ({ className }: any) => { className="w-full justify-start font-normal" > - Files + {t("common.files")} )} node.is_pinned)} Icon={} disableAdd @@ -134,7 +136,7 @@ export const SideBar = ({ className }: any) => { !node.parent_id && !node.is_deleted )} @@ -147,7 +149,7 @@ export const SideBar = ({ className }: any) => { disabled={!currentCut} > - Paste + {t("common.paste")} @@ -172,8 +174,6 @@ export const SideBar = ({ className }: any) => { - {/* */} - {/* */}
)} diff --git a/components/sidebar/trash/index.tsx b/components/sidebar/trash/index.tsx index 8dbab4ff..c3720b60 100644 --- a/components/sidebar/trash/index.tsx +++ b/components/sidebar/trash/index.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from "react" import { Trash2Icon, Undo2Icon } from "lucide-react" +import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" import { ITreeNode } from "@/lib/store/ITreeNode" @@ -31,6 +32,7 @@ import { ScrollArea } from "@/components/ui/scroll-area" export const Trash = () => { const [open, setOpen] = useState(false) + const { t } = useTranslation() const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) const allDeletedNodes = useAllNodes({ isDeleted: true }) const { restoreNode, permanentlyDeleteNode } = useSqlite() @@ -88,26 +90,29 @@ export const Trash = () => { asChild > - Trash + + {t("common.trash")} - Trash + {t("common.trash")} - restore or permanently delete nodes + {t("sidebar.trash.restoreOrPermanentlyDeleteNodes")} setSearch(e.target.value)} > - {!Boolean(allDeletedNodes.length) &&

Trash is empty

} - {!Boolean(allNodes.length) &&

no results found

} + {!Boolean(allDeletedNodes.length) && ( +

{t("sidebar.trash.trashIsEmpty")}

+ )} + {!Boolean(allNodes.length) &&

{t("common.noResultsFound")}

} {allNodes.map((node) => { return (
{ - Are you absolutely sure? + + {t("common.areYouAbsolutelySure")} + - This action cannot be undone. This will permanently delete the - node + {t("sidebar.trash.thisActionCannotBeUndone")} - Cancel + {t("common.cancel")} - Continue + {t("common.continue")} diff --git a/components/sidebar/tree/create-node-trigger.tsx b/components/sidebar/tree/create-node-trigger.tsx index 10ff2852..c06004c8 100644 --- a/components/sidebar/tree/create-node-trigger.tsx +++ b/components/sidebar/tree/create-node-trigger.tsx @@ -1,4 +1,5 @@ import { Plus } from "lucide-react" +import { useTranslation } from "react-i18next" import { ITreeNode } from "@/lib/store/ITreeNode" import { useCurrentPathInfo } from "@/hooks/use-current-pathinfo" @@ -14,6 +15,7 @@ import { export const CreateNodeTrigger = ({ parent_id }: { parent_id?: string }) => { const { space } = useCurrentPathInfo() + const { t } = useTranslation() const { createDoc, createTable, createFolder } = useSqlite(space) const goto = useGoto() @@ -59,21 +61,21 @@ export const CreateNodeTrigger = ({ parent_id }: { parent_id?: string }) => { handleCreateNode("doc") }} > - New Doc + {t("node.menu.newDoc")} { handleCreateNode("table") }} > - New Table + {t("node.menu.newTable")} { handleCreateNode("folder") }} > - New Folder + {t("node.menu.newFolder")} diff --git a/components/sidebar/tree/node-menu.tsx b/components/sidebar/tree/node-menu.tsx index 3f44d9d2..f8f64815 100644 --- a/components/sidebar/tree/node-menu.tsx +++ b/components/sidebar/tree/node-menu.tsx @@ -13,6 +13,7 @@ import { ScissorsIcon, Trash2Icon, } from "lucide-react" +import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" import { isInkServiceMode } from "@/lib/env" @@ -56,6 +57,7 @@ export function NodeItem({ node, depth, }: INodeItemProps) { + const { t } = useTranslation() const { createDoc, createTable, @@ -89,10 +91,10 @@ export function NodeItem({ useClickAway(() => { if (renameOpen) { - renameNode(node.id, newName); - setRenameOpen(false); + renameNode(node.id, newName) + setRenameOpen(false) } - }, [renameInputRef]); + }, [renameInputRef]) const router = useNavigate() @@ -110,12 +112,12 @@ export function NodeItem({ const handleRenameKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { - renameNode(node.id, newName); - setRenameOpen(false); + renameNode(node.id, newName) + setRenameOpen(false) } if (e.key === "Escape") { - setRenameOpen(false); - setNewName(node.name); // Reset to original name when canceling + setRenameOpen(false) + setNewName(node.name) // Reset to original name when canceling } } if (isInkServiceMode) { @@ -145,11 +147,11 @@ export function NodeItem({ - Delete + {t("common.delete")} - Rename + {t("node.menu.rename")} - {currentCut === node.id ? "Cancel cut" : "Cut"} + {currentCut === node.id + ? t("node.menu.cancelCut") + : t("node.menu.cut")} {node.type === "folder" && ( @@ -166,7 +170,7 @@ export function NodeItem({ disabled={!currentCut} > - Paste + {t("common.paste")} )} @@ -175,12 +179,12 @@ export function NodeItem({ {node.is_pinned ? ( unpin(node.id)}> - Unpin + {t("node.menu.unpin")} ) : ( pin(node.id)}> - Pin + {t("node.menu.pin")} )} @@ -191,15 +195,15 @@ export function NodeItem({ <> - New Doc + {t("node.menu.newDoc")} - New Table + {t("node.menu.newTable")} 6}> - New Nested Folder + {t("node.menu.newNestedFolder")} )} @@ -210,8 +214,7 @@ export function NodeItem({ disabled > - Duplicate - {/* ⌘R */} + {t("node.menu.duplicate")} )} @@ -220,7 +223,7 @@ export function NodeItem({ - Move Into + {t("node.menu.moveInto")} diff --git a/components/space-settings/index.tsx b/components/space-settings/index.tsx index d189bf01..bace9825 100644 --- a/components/space-settings/index.tsx +++ b/components/space-settings/index.tsx @@ -1,6 +1,7 @@ import { useState } from "react" import { SettingsIcon } from "lucide-react" import { Link, useNavigate } from "react-router-dom" +import { useTranslation } from "react-i18next" import { useCurrentPathInfo } from "@/hooks/use-current-pathinfo" import { useSpace } from "@/hooks/use-space" @@ -29,6 +30,7 @@ import { Label } from "@/components/ui/label" import { Button } from "../ui/button" export function Settings() { + const { t } = useTranslation() const { space } = useCurrentPathInfo() const { exportSpace, deleteSpace, rebuildIndex } = useSpace() const navigate = useNavigate() @@ -65,26 +67,25 @@ export function Settings() { return ( - Space Settings + {t('space.settings.title')} - Settings only apply to this space. if you want to change settings for - all spaces, go to{" "} + {t('space.settings.description')}{" "} - global settings + {t('space.settings.globalSettings')}
- +

- +

- Export all data from this space for backup or transfer purposes. + {t('space.settings.exportDescription')}

- +

- Reconstruct the search index for this space. Use this if you're - experiencing search issues. + {t('space.settings.rebuildIndexDescription')}

- +

- Permanently delete this space and all its contents. This action - cannot be undone. + {t('space.settings.deleteSpaceDescription')}

- Are you absolutely sure? + {t('common.areYouAbsolutelySure')} - This action cannot be undone. This will permanently delete - your space{" "} - {space}. - Please type the space name to confirm. + {t('space.settings.deleteSpaceWarning', { spaceName: space })} setConfirmName(e.target.value)} /> - Cancel + {t('common.cancel')} - Continue + {t('common.continue')} @@ -159,6 +155,7 @@ export function Settings() { } export const SpaceSettings = () => { + const { t } = useTranslation() return ( @@ -169,7 +166,7 @@ export const SpaceSettings = () => { asChild > - Settings + {t('common.settings')} diff --git a/components/table/field-selector.tsx b/components/table/field-selector.tsx index fd9489f5..d2823f5e 100644 --- a/components/table/field-selector.tsx +++ b/components/table/field-selector.tsx @@ -1,5 +1,6 @@ import { useState } from "react" import { ChevronsUpDown } from "lucide-react" +import { useTranslation } from "react-i18next" import { IField } from "@/lib/store/interface" import { @@ -32,10 +33,13 @@ export const FieldSelector = ({ onChange, }: IFieldSelectorProps) => { const [open, setOpen] = useState(false) + const { t } = useTranslation() + const handleSelect = (field: IField) => { onChange(field.table_column_name) setOpen(false) } + return ( @@ -47,16 +51,16 @@ export const FieldSelector = ({
{value ? fields.find((field) => field.table_column_name === value) - ?.name || "Untitled Field" - : "Select Field"} + ?.name || t("table.field.untitledField") + : t("table.field.selectField")}
- - No field found. + + {t("table.field.noFieldFound")} {fields.map((field) => { const iconSvgString = icons[field.type]({ @@ -76,7 +80,7 @@ export const FieldSelector = ({ }} >

- {field.name || "Untitled Field"} + {field.name || t("table.field.untitledField")}

diff --git a/components/table/view-editor/view-editor.tsx b/components/table/view-editor/view-editor.tsx index 39764586..480b3ead 100644 --- a/components/table/view-editor/view-editor.tsx +++ b/components/table/view-editor/view-editor.tsx @@ -3,6 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useClickAway } from "ahooks" import { useForm } from "react-hook-form" import * as z from "zod" +import { useTranslation } from "react-i18next" import { IView, ViewTypeEnum } from "@/lib/store/IView" import { Input } from "@/components/ui/input" @@ -36,6 +37,7 @@ interface IViewEditorProps { const LIMIT_ROWS_FOR_OPTIMIZE_VIEW = 88888 export const ViewEditor = ({ setEditDialogOpen, view }: IViewEditorProps) => { + const { t } = useTranslation() const ref = useRef(null) const { updateView } = useViewOperation() const { count, loading } = useViewCount(view) @@ -83,8 +85,7 @@ export const ViewEditor = ({ setEditDialogOpen, view }: IViewEditorProps) => { name="name" render={({ field }) => ( - Name - + {t('common.name')} @@ -98,9 +99,9 @@ export const ViewEditor = ({ setEditDialogOpen, view }: IViewEditorProps) => { name="type" render={({ field }) => ( - Type + {t('table.fieldType')} - The type of view to use for this table. + {t('table.view.typeDescription')}
@@ -111,7 +112,7 @@ export const ViewEditor = ({ setEditDialogOpen, view }: IViewEditorProps) => { }} viewId={view.id} isActive={field.value === "grid"} - title="Grid" + title={t('table.view.grid')} viewType={ViewTypeEnum.Grid} icon={ViewIconMap[ViewTypeEnum.Grid]} > @@ -123,7 +124,7 @@ export const ViewEditor = ({ setEditDialogOpen, view }: IViewEditorProps) => { disabled={disabled} viewId={view.id} isActive={field.value === "gallery"} - title="Gallery" + title={t('table.view.gallery')} viewType={ViewTypeEnum.Gallery} icon={ViewIconMap[ViewTypeEnum.Gallery]} > @@ -135,13 +136,13 @@ export const ViewEditor = ({ setEditDialogOpen, view }: IViewEditorProps) => { }} disabled={disabled} isActive={field.value === "doc_list"} - title="Doc list (beta)" + title={t('table.view.docList')} viewType={ViewTypeEnum.DocList} icon={ViewIconMap[ViewTypeEnum.DocList]} > {disabled && (

- The disabled view types are not ready for large data + {t('table.view.disabledViewTypesWarning')}

)}
@@ -156,9 +157,9 @@ export const ViewEditor = ({ setEditDialogOpen, view }: IViewEditorProps) => { name="query" render={({ field }) => ( - Query + {t('table.view.query')} - sql query to use for this view. + {t('table.view.queryDescription')} @@ -167,7 +168,7 @@ export const ViewEditor = ({ setEditDialogOpen, view }: IViewEditorProps) => { )} /> - +
diff --git a/components/table/view-field/view-field.tsx b/components/table/view-field/view-field.tsx index d7bb04a9..58c7af01 100644 --- a/components/table/view-field/view-field.tsx +++ b/components/table/view-field/view-field.tsx @@ -5,6 +5,7 @@ import { sortBy } from "lodash" import { ArrowDownUpIcon, SlidersHorizontalIcon } from "lucide-react" import { DndProvider } from "react-dnd" import { HTML5Backend } from "react-dnd-html5-backend" +import { useTranslation } from "react-i18next" import { IView } from "@/lib/store/IView" import { IField } from "@/lib/store/interface" @@ -26,6 +27,7 @@ export interface ContainerState { } export const ViewField = (props: { view?: IView }) => { + const { t } = useTranslation() const [open, setOpen] = useState(false) const orderMap = useMemo( () => props.view?.order_map || {}, @@ -146,10 +148,10 @@ export const ViewField = (props: { view?: IView }) => {

@@ -161,7 +163,7 @@ export const ViewField = (props: { view?: IView }) => {
- Add Field + {t('table.view.field.addField')}
diff --git a/components/table/view-filter-editor/view-filter-editor.tsx b/components/table/view-filter-editor/view-filter-editor.tsx index da1dd1c2..b0245c56 100644 --- a/components/table/view-filter-editor/view-filter-editor.tsx +++ b/components/table/view-filter-editor/view-filter-editor.tsx @@ -1,4 +1,5 @@ import { CopyPlusIcon, PlusIcon } from "lucide-react" +import { useTranslation } from "react-i18next" import { BinaryOperator, CompareOperator } from "@/lib/fields/const" import { IField } from "@/lib/store/interface" @@ -32,6 +33,8 @@ export const ViewFilterEditor = ({ handleClearFilter, depth = 0, }: IViewFilterEditorProps) => { + const { t } = useTranslation() + const handleAddFilter = () => { const newValue = _value ? { @@ -94,12 +97,12 @@ export const ViewFilterEditor = ({ className="flex cursor-pointer items-center gap-2 rounded-sm p-2 hover:bg-secondary" > - add filter + {t('table.view.addFilter')}
) : ( - add filter + {t('table.view.addFilter')}
- add filter + {t('table.view.addFilter')}
{depth < 2 && (
- add group filter + {t('table.view.addGroupFilter')}
)}
@@ -131,12 +134,12 @@ export const ViewFilterEditor = ({ })} >
- There is no filter rule, add one + {t('table.view.noFilterRule')} {AddFilterComponent}
- delete filter + {t('table.view.deleteFilter')}
) @@ -164,7 +167,7 @@ export const ViewFilterEditor = ({ {AddFilterComponent}
) diff --git a/components/table/view-filter-editor/view-filter-group-editor.tsx b/components/table/view-filter-editor/view-filter-group-editor.tsx index 59d3c719..efdf0e17 100644 --- a/components/table/view-filter-editor/view-filter-group-editor.tsx +++ b/components/table/view-filter-editor/view-filter-group-editor.tsx @@ -1,5 +1,6 @@ import React from "react" import { Trash2Icon } from "lucide-react" +import { useTranslation } from "react-i18next" import { BinaryOperator } from "@/lib/fields/const" import { isLogicOperator } from "@/lib/sqlite/sql-filter-parser" @@ -30,6 +31,7 @@ export const ViewFilterGroupEditor = ({ depth = 0, parentOperator, }: IViewFilterGroupEditorProps) => { + const { t } = useTranslation() const handleValueChange = (value: IGroupFilterValue, index: number) => { const newValue = { ..._value, operands: [..._value.operands] } newValue.operands[index] = value @@ -54,7 +56,7 @@ export const ViewFilterGroupEditor = ({ {_value?.operands.map((operand, index) => { return ( - {index === 0 &&
Where
} + {index === 0 &&
{t("table.view.where")}
} {index === 1 && ( void }) => { + const { t } = useTranslation() return ( diff --git a/components/table/view-item.tsx b/components/table/view-item.tsx index 9b5a9593..99167d98 100644 --- a/components/table/view-item.tsx +++ b/components/table/view-item.tsx @@ -1,6 +1,7 @@ import { useContext, useState } from "react" import { LayoutGridIcon, LayoutListIcon, Table2Icon } from "lucide-react" import ReactDOM from "react-dom" +import { useTranslation } from "react-i18next" import { IView, ViewTypeEnum } from "@/lib/store/IView" import { cn } from "@/lib/utils" @@ -47,6 +48,7 @@ export const ViewItem = ({ deleteView, disabledDelete, }: IViewItemProps) => { + const { t } = useTranslation() const [open, setOpen] = useState(false) const { getLoading } = useViewLoadingStore() const loading = getLoading(view.query) @@ -92,31 +94,30 @@ export const ViewItem = ({ - Edit + {t('table.view.edit')} - Delete + {t('common.delete')} - Are you sure delete this view? + {t('table.view.deleteConfirmTitle')} - This action cannot be undone. This will permanently delete the - view + {t('table.view.deleteConfirmDescription')} diff --git a/components/table/view-sort-editor.tsx b/components/table/view-sort-editor.tsx index e7d11ca5..16b5239b 100644 --- a/components/table/view-sort-editor.tsx +++ b/components/table/view-sort-editor.tsx @@ -1,5 +1,6 @@ import { useCallback, useContext, useEffect, useMemo, useState } from "react" import { XIcon } from "lucide-react" +import { useTranslation } from "react-i18next" import { useUiColumns } from "@/hooks/use-ui-columns" import { Button } from "@/components/ui/button" @@ -24,6 +25,7 @@ interface IViewEditorProps { onSortChange?: (sort: OrderByItem[]) => void } export function ViewSortEditor(props: IViewEditorProps) { + const { t } = useTranslation() const { onSortChange } = props const { tableName, space, viewId } = useContext(TableContext) const { currentView } = useCurrentView({ tableName, space, viewId }) @@ -108,13 +110,12 @@ export function ViewSortEditor(props: IViewEditorProps) {
{!orderItems.length && ( - There is no sort rule, add one + {t("table.view.noSortRule")} )} {orderItems.map((item, index) => { return (
- {/* */} onOrderChange(value, index)} > - + - Ascending - Descending + {t("table.sortAscending")} + + {t("table.sortDescending")} +
diff --git a/components/table/view-toolbar.tsx b/components/table/view-toolbar.tsx index c26984ff..9f582264 100644 --- a/components/table/view-toolbar.tsx +++ b/components/table/view-toolbar.tsx @@ -39,6 +39,7 @@ import { ViewField } from "./view-field/view-field" import { ViewFilter } from "./view-filter" import { ViewItem } from "./view-item" import { ViewSort } from "./view-sort" +import { useTranslation } from "react-i18next" const useGap = ( width: number | undefined, @@ -158,6 +159,7 @@ export const ViewToolbar = (props: { const [open, setOpen] = useState(false) const tableId = getTableIdByRawTableName(tableName) const { subPageId, setSubPage, clearSubPage } = useCurrentSubPage() + const { t } = useTranslation() const handleAddRow = async () => { const uuid = uuidv7() @@ -261,7 +263,7 @@ export const ViewToolbar = (props: { {!props.isReadOnly && ( )} diff --git a/components/table/views/gallery/properties.tsx b/components/table/views/gallery/properties.tsx index 41506c59..c6c6d14b 100644 --- a/components/table/views/gallery/properties.tsx +++ b/components/table/views/gallery/properties.tsx @@ -2,6 +2,7 @@ import { useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { z } from "zod" +import { useTranslation } from "react-i18next" import { Button } from "@/components/ui/button" import { @@ -42,22 +43,15 @@ export const GalleryViewProperties = (props: { viewId: string }) => { }, }) const [popoverOpen, setPopoverOpen] = useState(false) + const { t } = useTranslation() const onSubmit = (data: IGalleryViewProperties) => console.log(data) const fileFields = useFileFields() const coverPreviewItems = [ - // { - // value: null, - // label: "None", - // }, - // { - // value: "cover", - // label: "Cover", - // }, { value: "content", - label: "Content", + label: t("table.view.gallery.content"), }, ...fileFields.map((field) => ({ value: field.table_column_name, @@ -78,7 +72,7 @@ export const GalleryViewProperties = (props: { viewId: string }) => { name="hideEmptyFields" render={({ field }) => ( - Hide empty fields + {t("table.view.gallery.hideEmptyFields")} { @@ -101,7 +95,7 @@ export const GalleryViewProperties = (props: { viewId: string }) => { name="coverPreview" render={({ field }) => ( - Cover preview + {t("table.view.gallery.coverPreview")} diff --git a/components/table/views/grid/fields/field-append-panel.tsx b/components/table/views/grid/fields/field-append-panel.tsx index 7d77bac6..07990a8f 100644 --- a/components/table/views/grid/fields/field-append-panel.tsx +++ b/components/table/views/grid/fields/field-append-panel.tsx @@ -1,5 +1,6 @@ import * as React from "react" import { useClickAway } from "ahooks" +import { useTranslation } from "react-i18next" import { BaselineIcon, CalendarDaysIcon, @@ -40,58 +41,58 @@ export function FieldAppendPanel({ ) => Promise uiColumns: IField[] }) { + const { t } = useTranslation() const [currentField, setCurrentField] = React.useState() const { tableName } = useCurrentPathInfo() const ref = React.useRef(null) const { isAddFieldEditorOpen, setIsAddFieldEditorOpen } = useTableAppStore() const fieldTypes = [ - { name: "Text", value: FieldType.Text, icon: BaselineIcon }, - { name: "Number", value: FieldType.Number, icon: HashIcon }, - { name: "Select", value: FieldType.Select, icon: TagIcon }, - { name: "MultiSelect", value: FieldType.MultiSelect, icon: TagsIcon }, + { name: t("table.field.text"), value: FieldType.Text, icon: BaselineIcon }, + { name: t("table.field.number"), value: FieldType.Number, icon: HashIcon }, + { name: t("table.field.select"), value: FieldType.Select, icon: TagIcon }, + { name: t("table.field.multiSelect"), value: FieldType.MultiSelect, icon: TagsIcon }, { - name: "Checkbox", + name: t("table.field.checkbox"), value: FieldType.Checkbox, icon: CheckSquareIcon, }, - { name: "Rating", value: FieldType.Rating, icon: StarIcon }, - - { name: "URL", value: FieldType.URL, icon: Link2Icon }, - { name: "Date", value: FieldType.Date, icon: CalendarDaysIcon }, - { name: "Files", value: FieldType.File, icon: ImageIcon }, + { name: t("table.field.rating"), value: FieldType.Rating, icon: StarIcon }, + { name: t("table.field.url"), value: FieldType.URL, icon: Link2Icon }, + { name: t("table.field.date"), value: FieldType.Date, icon: CalendarDaysIcon }, + { name: t("table.field.file"), value: FieldType.File, icon: ImageIcon }, { - name: "Formula", + name: t("table.field.formula"), value: FieldType.Formula, icon: SigmaIcon, }, { - name: "Link", + name: t("table.field.link"), value: FieldType.Link, icon: LinkIcon, disable: false, }, { - name: "Lookup", + name: t("table.field.lookup"), value: FieldType.Lookup, icon: TextSearchIcon, }, { - name: "Created Time", + name: t("table.field.createdTime"), value: FieldType.CreatedTime, icon: Clock3Icon, }, { - name: "Last Edited Time", + name: t("table.field.lastEditedTime"), value: FieldType.LastEditedTime, icon: Clock3Icon, }, { - name: "Created By", + name: t("table.field.createdBy"), value: FieldType.CreatedBy, icon: UserIcon, }, { - name: "Last Edited By", + name: t("table.field.lastEditedBy"), value: FieldType.LastEditedBy, icon: UserIcon, }, @@ -187,7 +188,7 @@ export function FieldAppendPanel({ ) : (

- add field + {t("table.field.addField")}

{fieldTypes.map((field, i) => { diff --git a/components/table/views/grid/fields/field-delete.tsx b/components/table/views/grid/fields/field-delete.tsx index adc25b95..9f37521f 100644 --- a/components/table/views/grid/fields/field-delete.tsx +++ b/components/table/views/grid/fields/field-delete.tsx @@ -1,4 +1,5 @@ import { useState } from "react" +import { useTranslation } from "react-i18next" import { FieldType } from "@/lib/fields/const" import { IField } from "@/lib/store/interface" @@ -24,6 +25,7 @@ export const FieldDelete = ({ children, deleteField, }: IFieldDeleteProps) => { + const { t } = useTranslation() const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const handleDeleteFieldConfirm = () => { deleteField(field.table_column_name) @@ -34,19 +36,19 @@ export const FieldDelete = ({ {children} - Are you sure delete this field? + {t('table.field.deleteConfirmTitle')} {field.type === FieldType.Link - ? "This field is a link field. Deleting this field will also delete the paired field. and this action cannot be undone." - : "This action cannot be undone."} + ? t('table.field.deleteLinkFieldWarning') + : t('common.thisActionCannotBeUndone')} diff --git a/components/table/views/grid/fields/field-editor-dropdown.tsx b/components/table/views/grid/fields/field-editor-dropdown.tsx index d2413d11..4e17fcd2 100644 --- a/components/table/views/grid/fields/field-editor-dropdown.tsx +++ b/components/table/views/grid/fields/field-editor-dropdown.tsx @@ -7,6 +7,7 @@ import { Trash2, } from "lucide-react" import { useLayer } from "react-laag" +import { useTranslation } from 'react-i18next'; import { FieldType } from "@/lib/fields/const" import { IView } from "@/lib/store/IView" @@ -60,6 +61,7 @@ export const FieldEditorDropdown = (props: IFieldEditorDropdownProps) => { const inputRef = useRef(null) const { fields } = useTableFields(tableName) const { showColumns } = useColumns(fields, props.view) + const { t } = useTranslation(); useEffect(() => { const currentField = showColumns[currentColIndex!] @@ -167,15 +169,15 @@ export const FieldEditorDropdown = (props: IFieldEditorDropdownProps) => { onClick={handleEditFieldPropertiesClick} > - Edit Property + {t('table.editProperty')} - Sort Ascending + {t('table.sortAscending')} - Sort Descending + {t('table.sortDescending')} {currentUiColumn?.type !== "title" && ( { > - Delete Field + {t('table.deleteField')} )} - Are you sure delete this field? + {t('table.deleteFieldConfirmation')} {currentUiColumn?.type === FieldType.Link - ? "This field is a link field. Deleting this field will also delete the paired field. and this action cannot be undone." - : "This action cannot be undone."} + ? t('table.deleteLinkFieldWarning') + : t('common.thisActionCannotBeUndone')} @@ -202,13 +204,13 @@ export const FieldEditorDropdown = (props: IFieldEditorDropdownProps) => { variant="ghost" onClick={() => setIsDeleteDialogOpen(false)} > - Cancel + {t('common.cancel')} diff --git a/components/table/views/grid/fields/field-property-editor.tsx b/components/table/views/grid/fields/field-property-editor.tsx index 91d964a8..2bc9cba5 100644 --- a/components/table/views/grid/fields/field-property-editor.tsx +++ b/components/table/views/grid/fields/field-property-editor.tsx @@ -1,6 +1,7 @@ import React from "react" import { useClickAway } from "ahooks" import { Trash2 } from "lucide-react" +import { useTranslation } from 'react-i18next'; import { FieldType } from "@/lib/fields/const" import { IField } from "@/lib/store/interface" @@ -54,6 +55,7 @@ export const FieldPropertyEditor = ({ databaseName, deleteField, }: IFieldPropertyEditorProps) => { + const { t } = useTranslation(); const ref = React.useRef(null) const { setIsFieldPropertiesEditorOpen, currentUiColumn: currentField } = useTableAppStore() @@ -95,7 +97,7 @@ export const FieldPropertyEditor = ({
- +
- + - Delete Field + {t('table.deleteField')} )} diff --git a/components/table/views/grid/fields/field-type-select.tsx b/components/table/views/grid/fields/field-type-select.tsx index 87e70ddd..49badde6 100644 --- a/components/table/views/grid/fields/field-type-select.tsx +++ b/components/table/views/grid/fields/field-type-select.tsx @@ -1,5 +1,6 @@ import * as React from "react" import { Check, ChevronsUpDown } from "lucide-react" +import { useTranslation } from "react-i18next" import { FieldType } from "@/lib/fields/const" import { cn } from "@/lib/utils" @@ -20,77 +21,26 @@ import { FieldIcon } from "@/components/table/field-icon" // for now only support these fields const fields = [ - { - value: FieldType.Text, - label: "Text", - }, - { - value: FieldType.Number, - label: "Number", - }, - { - value: FieldType.Select, - label: "Select", - }, - { - value: FieldType.MultiSelect, - label: "Multi-select", - }, - { - value: FieldType.Checkbox, - label: "Checkbox", - }, - { - value: FieldType.Rating, - label: "Rating", - }, - { - value: FieldType.URL, - label: "URL", - }, - { - value: FieldType.Date, - label: "Date", - }, - { - value: FieldType.File, - label: "File", - }, + { value: FieldType.Text, label: "table.field.text" }, + { value: FieldType.Number, label: "table.field.number" }, + { value: FieldType.Select, label: "table.field.select" }, + { value: FieldType.MultiSelect, label: "table.field.multiSelect" }, + { value: FieldType.Checkbox, label: "table.field.checkbox" }, + { value: FieldType.Rating, label: "table.field.rating" }, + { value: FieldType.URL, label: "table.field.url" }, + { value: FieldType.Date, label: "table.field.date" }, + { value: FieldType.File, label: "table.field.file" }, ] const readonlyFields = [ - { - value: FieldType.Title, - label: "Title", - }, - { - value: FieldType.Formula, - label: "Formula", - }, - { - value: FieldType.Link, - label: "Link", - }, - { - value: FieldType.Lookup, - label: "Lookup", - }, - { - value: FieldType.CreatedTime, - label: "Created Time", - }, - { - value: FieldType.LastEditedTime, - label: "Last Edited Time", - }, - { - value: FieldType.CreatedBy, - label: "Created By", - }, - { - value: FieldType.LastEditedBy, - label: "Last Edited By", - }, + { value: FieldType.Title, label: "table.field.title" }, + { value: FieldType.Formula, label: "table.field.formula" }, + { value: FieldType.Link, label: "table.field.link" }, + { value: FieldType.Lookup, label: "table.field.lookup" }, + { value: FieldType.CreatedTime, label: "table.field.createdTime" }, + { value: FieldType.LastEditedTime, label: "table.field.lastEditedTime" }, + { value: FieldType.CreatedBy, label: "table.field.createdBy" }, + { value: FieldType.LastEditedBy, label: "table.field.lastEditedBy" }, ] interface IFieldTypeSelectProps { @@ -100,6 +50,7 @@ interface IFieldTypeSelectProps { export function FieldTypeSelect({ value, onChange }: IFieldTypeSelectProps) { const [open, setOpen] = React.useState(false) + const { t } = useTranslation() const canBeSelected = fields.some((field) => field.value === value) return ( @@ -115,22 +66,22 @@ export function FieldTypeSelect({ value, onChange }: IFieldTypeSelectProps) { {value ? (
- { + {t( [...fields, ...readonlyFields].find( (field) => field.value === value - )?.label - } + )?.label || "" + )}
) : ( - "Select field..." + t("table.field.selectField") )} - - No field found. + + {t("table.field.noFieldFound")} {fields.map((field) => (
- {field.label} + {t(field.label)}
))} diff --git a/lib/env.ts b/lib/env.ts index ad389b7d..42c324b0 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -1,5 +1,5 @@ export const logger = console -export const EIDOS_VERSION = "0.7.9" +export const EIDOS_VERSION = "0.8.0" export const isDevMode = Boolean(import.meta.env?.DEV) export const isSelfHosted = import.meta.env?.VITE_EIDOS_SELF_HOSTED === "true" export const isInkServiceMode = diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 00000000..4dd86154 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,321 @@ +{ + "aiChat.inputEditor.pressAtToMentionResource": "@ to mention resource", + "aiChat.inputEditor.pressSlashToSwitchPrompt": "Press / to switch prompt", + "aiChat.inputEditor.typeYourMessageHere": "Type your message here.", + "cmdk.inputPlaceholder": "Type a command or search... (type / for scripts)", + "cmdk.newDraftDoc": "New Draft Doc", + "cmdk.notFound": "Not found \"{{input}}\"", + "cmdk.suggestions": "Suggestions", + "cmdk.switchTheme": "Switch Theme", + "common.ai": "AI", + "common.apiKey": "API Key", + "common.areYouAbsolutelySure": "Are you absolutely sure?", + "common.cancel": "Cancel", + "common.continue": "Continue", + "common.delete": "Delete", + "common.download": "Download", + "common.error": "Error", + "common.esc": "ESC", + "common.export": "Export", + "common.extensions": "Extensions", + "common.fetch": "Fetch", + "common.files": "Files", + "common.filter": "Filter...", + "common.import": "Import", + "common.importFileDescription": "Choose a file to import into your workspace.", + "common.lock": "Locked", + "common.name": "Name", + "common.new": "New", + "common.noResultsFound": "No results found", + "common.noTableFound": "No table found.", + "common.nodes": "Nodes", + "common.open": "Open", + "common.paste": "Paste", + "common.pinned": "Pinned", + "common.remove": "Remove", + "common.restore": "Restore", + "common.search": "Search", + "common.select": "Select", + "common.settings": "Settings", + "common.shareMode": "Share Mode", + "common.system": "System", + "common.test": "Test", + "common.thisActionCannotBeUndone": "This action cannot be undone.", + "common.today": "Today", + "common.trash": "Trash", + "common.update": "Update", + "doc.addCover": "Add Cover", + "doc.addIcon": "Add Icon", + "doc.changeCover": "Change cover", + "doc.coverImage": "Cover image", + "doc.hideProperties": "Hide Properties", + "doc.menu.background": "Background {{name}}", + "doc.menu.bookmark": "Bookmark", + "doc.menu.bulletedList": "Bulleted List", + "doc.menu.checkList": "Check List", + "doc.menu.code": "Code", + "doc.menu.color": "Color {{name}}", + "doc.menu.databaseTable": "Database Table", + "doc.menu.divider": "Divider", + "doc.menu.heading": "Heading {{n}}", + "doc.menu.image": "Image", + "doc.menu.insertTable": "{{rows}}x{{columns}} Table", + "doc.menu.numberedList": "Numbered List", + "doc.menu.paragraph": "Paragraph", + "doc.menu.query": "Query", + "doc.menu.quote": "Quote", + "doc.menu.tableOfContent": "Table Of Content", + "doc.nodeInTrash": "This node is in the trash", + "doc.permanentDeleteWarning": "This action cannot be undone. This will permanently delete the node", + "doc.pressForCommand": "press / for Command", + "doc.showProperties": "Show Properties", + "doc.untitled": "Untitled", + "everyday.import.importing": "Importing...", + "everyday.import.selectFolder": "Click here to select Logseq journal folder", + "everyday.import.title": "Import From Logseq (Beta)", + "kbd.shortcuts.common.description": "Description", + "kbd.shortcuts.common.openSettingsDescription": "Opens the settings menu", + "kbd.shortcuts.common.shortcut": "Shortcut", + "kbd.shortcuts.common.title": "Common Keyboard Shortcuts", + "kbd.shortcuts.common.toggleChatbotDescription": "Opens or closes the chatbot interface", + "kbd.shortcuts.common.toggleCommandPaletteDescription": "Opens or closes the command palette", + "kbd.shortcuts.common.toggleSidebarDescription": "Shows or hides the sidebar", + "kbd.shortcuts.common.toggleThemeDescription": "Switches between light and dark themes", + "kbd.shortcuts.document.checkboxDescription": "Creates a checkbox", + "kbd.shortcuts.document.codeBlockDescription": "Creates a code block", + "kbd.shortcuts.document.ctrlBDescription": "Toggles bold formatting for selected text", + "kbd.shortcuts.document.ctrlIDescription": "Toggles italic formatting for selected text", + "kbd.shortcuts.document.ctrlSDescription": "Saves the current document", + "kbd.shortcuts.document.ctrlUDescription": "Toggles underline formatting for selected text", + "kbd.shortcuts.document.heading1Description": "Creates a level 1 heading", + "kbd.shortcuts.document.heading2Description": "Creates a level 2 heading", + "kbd.shortcuts.document.heading3Description": "Creates a level 3 heading", + "kbd.shortcuts.document.horizontalRuleDescription": "Inserts a horizontal rule", + "kbd.shortcuts.document.orderedListDescription": "Creates an ordered list item", + "kbd.shortcuts.document.title": "Document Keyboard Shortcuts", + "kbd.shortcuts.document.unorderedListDescription": "Creates an unordered list item", + "kbd.shortcuts.table.altArrowDescription": "Moves the currently selected cell and retains the current selection", + "kbd.shortcuts.table.arrowDescription": "Moves the currently selected cell and clears other selections", + "kbd.shortcuts.table.ctrlADescription": "Selects the entire table", + "kbd.shortcuts.table.ctrlArrowDescription": "Moves to the edge of the current data region in the direction pressed", + "kbd.shortcuts.table.ctrlCDescription": "Copies the selected cells", + "kbd.shortcuts.table.ctrlDDescription": "Fills the selected cells with the value of the topmost selected cell", + "kbd.shortcuts.table.ctrlFDescription": "Opens the search dialog", + "kbd.shortcuts.table.ctrlHomeEndDescription": "Moves to the first or last cell of the table", + "kbd.shortcuts.table.ctrlRDescription": "Fills the selected cells with the value of the leftmost selected cell", + "kbd.shortcuts.table.ctrlShiftArrowDescription": "Extends the current selection to the edge of the current data region in the direction pressed", + "kbd.shortcuts.table.ctrlShiftHomeEndDescription": "Selects from the current cell to the first or last cell of the table", + "kbd.shortcuts.table.ctrlSpaceDescription": "Selects the entire column", + "kbd.shortcuts.table.ctrlVDescription": "Pastes the copied cells", + "kbd.shortcuts.table.escapeDescription": "Deselects all selected cells", + "kbd.shortcuts.table.pageUpDownDescription": "Moves one page up or down", + "kbd.shortcuts.table.shiftArrowDescription": "Extends the current selection range in the direction pressed", + "kbd.shortcuts.table.shiftHomeEndDescription": "Extends the selection to the beginning or end of the current row", + "kbd.shortcuts.table.shiftSpaceDescription": "Selects the entire row", + "kbd.shortcuts.table.title": "Table(Grid View) Keyboard Shortcuts", + "nav.dropdown.menu.commandPalette": "Command Palette", + "nav.dropdown.menu.creatingEmbedding": "Creating Embedding for {{name}}", + "nav.dropdown.menu.desktop": "Desktop", + "nav.dropdown.menu.emailInstructions": "1. Scan the QR code to add the address to your contacts\n2. Send an email to this address to save data into this table", + "nav.dropdown.menu.embedding": "Embedding(Beta)", + "nav.dropdown.menu.embeddingCreated": "Embedding Created", + "nav.dropdown.menu.fullWidth": "Full Width", + "nav.dropdown.menu.keyboardShortcuts": "Keyboard Shortcuts", + "nav.dropdown.menu.lastUpdated": "Last updated: {{time}}", + "nav.dropdown.menu.lock": "Lock", + "nav.dropdown.menu.mail": "Mail", + "nav.dropdown.menu.sendMailToEidos": "Send mail to Eidos", + "nav.dropdown.menu.version": "Version: {{version}} ({{mode}})", + "nav.dropdown.menu.web": "Web", + "nav.dropdown.menu.website": "Website", + "nav.status.apiAgentConnected": "API Agent Connected", + "nav.status.checkForUpdates": "Check for updates", + "nav.status.clickToPin": "Click to pin this node", + "nav.status.clickToUnpin": "Click to unpin this node", + "nav.status.noApiAgentConnected": "No API Agent Connected", + "nav.status.noUpdatesAvailable": "No updates available", + "nav.status.nodeLocked": "This node is locked (read-only)", + "nav.status.updateAvailable": "Update to v{{version}}", + "node.menu.cancelCut": "Cancel cut", + "node.menu.cut": "Cut", + "node.menu.duplicate": "Duplicate", + "node.menu.moveInto": "Move Into", + "node.menu.newDoc": "New Doc", + "node.menu.newFolder": "New Folder", + "node.menu.newNestedFolder": "New Nested Folder", + "node.menu.newTable": "New Table", + "node.menu.pin": "Pin", + "node.menu.rename": "Rename", + "node.menu.unpin": "Unpin", + "settings.ai": "AI", + "settings.ai.addProvider": "Add Provider", + "settings.ai.addProviderDescription": "Add a new LLM provider to your configuration.", + "settings.ai.apiKeyDescription": "Enter your API key for authentication.", + "settings.ai.baseUrl": "Base URL", + "settings.ai.baseUrlDescription": "The base URL for the API.", + "settings.ai.baseUrlRequired": "Base URL is required.", + "settings.ai.codingModel": "Coding Model", + "settings.ai.codingModelDescription": "Select your preferred model for coding tasks", + "settings.ai.configUpdated": "AI Config updated.", + "settings.ai.description": "Configure your AI settings.", + "settings.ai.embeddingModel": "Embedding Model", + "settings.ai.embeddingModelDescription": "Select your preferred model for embedding tasks", + "settings.ai.fetchModelListError": "Failed to fetch model list.", + "settings.ai.localLLMDescription": "Manage your local LLM.", + "settings.ai.localLLMTitle": "Local LLM", + "settings.ai.modelPreferences": "Model Preferences", + "settings.ai.modelPreferencesDescription": "Select preferred models for different tasks", + "settings.ai.models": "Models", + "settings.ai.modelsDescription": "Comma-separated list of model IDs.", + "settings.ai.provider": "Provider", + "settings.ai.providerDescription": "There are many LLM API providers. configure as your need.", + "settings.ai.providerType": "Provider Type", + "settings.ai.translationModel": "Translation Model", + "settings.ai.translationModelDescription": "Select your preferred model for translation tasks", + "settings.ai.updateLLMProvider": "Update LLM Provider", + "settings.api": "API", + "settings.api.agentUrl": "API Agent URL", + "settings.api.agentUrlDescription": "The URL of your API Agent.", + "settings.api.callApiThrough": "Call API through", + "settings.api.description": "Configure your API settings.", + "settings.api.enable": "Enable", + "settings.api.enableDescription": "When enabled, you can query data from Eidos Web APP through", + "settings.api.regenerate": "Regenerate", + "settings.api.update": "Update", + "settings.appearance": "Appearance", + "settings.appearance.dark": "Dark", + "settings.appearance.description": "Customize the appearance of the app. Automatically switch between day and night themes.", + "settings.appearance.font": "Font", + "settings.appearance.fontDescription": "Set the font you want to use in the dashboard.", + "settings.appearance.language": "Language", + "settings.appearance.languageDescription": "Select your preferred language.", + "settings.appearance.light": "Light", + "settings.appearance.submittedValues": "You submitted the following values:", + "settings.appearance.theme": "Theme", + "settings.appearance.themeDescription": "Select your preferred theme.", + "settings.appearance.title": "Appearance", + "settings.appearance.updatePreferences": "Update preferences", + "settings.devtools": "Devtools", + "settings.devtools.description": "Developer tools settings.", + "settings.experiment": "Experiment", + "settings.experiment.description": "Experimental features settings.", + "settings.general": "General", + "settings.general.clientId": "Client ID", + "settings.general.description": "How others will see you when collaborating.", + "settings.manageAppSettings": "Manage App Settings and Configuration", + "settings.security": "Security", + "settings.security.description": "Configure your security settings.", + "settings.storage": "Storage", + "settings.storage.autoBackupDescription": "Backup data every {{minutes}} minutes, 0 means disable auto save.", + "settings.storage.autoBackupDisabled": "Disable auto save.", + "settings.storage.autoBackupExplanation": "Backup every space's database to the local path. Keep data more secure.", + "settings.storage.autoBackupGap": "Auto backup gap (minutes)", + "settings.storage.dataFolder": "Data Folder", + "settings.storage.dataFolderDescription": "The folder where your data will be stored.", + "settings.storage.dataFolderNotSelected": "Data folder not selected", + "settings.storage.description": "Configure your storage settings.", + "settings.storage.fileSystem": "File System", + "settings.storage.fileSystemDescription": "Which file system to store your files. OPFS stores files in the browser's storage, while Native File System stores files in a local directory on your device.", + "settings.storage.grantPermission": "Grant Permission", + "settings.storage.nativeFileSystem": "Native File System", + "settings.storage.noTypeFound": "No type found.", + "settings.storage.permissionDenied": "Permission denied.", + "settings.storage.permissionGranted": "Permission granted.", + "settings.storage.searchType": "Search type...", + "settings.storage.selectDataFolder": "You need to select a data folder.", + "settings.storage.selectDataFolderPlaceholder": "Select a data folder", + "settings.storage.selectFileSystem": "Select File System", + "settings.storage.settingsUpdated": "Settings updated", + "settings.sync": "Sync", + "settings.sync.description": "Configure your sync settings.", + "settings.title": "Settings", + "sidebar.importFile.description": "Choose a file to import into your workspace.", + "sidebar.importFile.importCSV": "Import CSV", + "sidebar.importFile.importCSVDescription": "Import a CSV file to create a new table", + "sidebar.importFile.importMarkdown": "Import Markdown", + "sidebar.importFile.importMarkdownDescription": "Import a markdown file to create a new document", + "sidebar.importFile.title": "Import File", + "sidebar.trash.restoreOrPermanentlyDeleteNodes": "Restore or permanently delete nodes", + "sidebar.trash.thisActionCannotBeUndone": "This action cannot be undone. This will permanently delete the selected node.", + "sidebar.trash.trashIsEmpty": "Trash is empty", + "space.select.createNew": "Create New", + "space.select.createSpace": "Create Space", + "space.select.createSpaceDescription": "Add a new space to manage data for you", + "space.select.creating": "Creating", + "space.select.importFromFile": "Import from file", + "space.select.importFromFileDescription": "If you export space as a zip file, you can import it here", + "space.select.overwriteWarning": "It seems you are trying to overwrite an existing space. Please be careful, this will overwrite data in the existing space.", + "space.select.searchDatabase": "Search Database...", + "space.select.selectDatabase": "Select Database...", + "space.select.spaceAlreadyExists": "This space already exists, choose another name", + "space.select.spaceName": "Space name", + "space.select.spaceNamePlaceholder": "e.g. personal", + "space.settings.dangerZone": "Danger zone", + "space.settings.deleteSpace": "Delete Space", + "space.settings.deleteSpaceDescription": "Permanently delete this space and all its contents. This action cannot be undone.", + "space.settings.deleteSpaceWarning": "This action cannot be undone. This will permanently delete your space <1>{{spaceName}}. Please type the space name to confirm.", + "space.settings.description": "Settings only apply to this space. If you want to change settings for all spaces, go to", + "space.settings.exportDescription": "Export all data from this space for backup or transfer purposes.", + "space.settings.exportSpace": "Export Space", + "space.settings.globalSettings": "global settings", + "space.settings.rebuildIndex": "Rebuild Index", + "space.settings.rebuildIndexDescription": "Reconstruct the search index for this space. Use this if you're experiencing search issues.", + "space.settings.rebuilding": "Rebuilding...", + "space.settings.title": "Space Settings", + "space.settings.typeSpaceName": "Type space name", + "table.deleteField": "Delete Field", + "table.deleteFieldConfirmation": "Are you sure you want to delete this field?", + "table.deleteLinkFieldWarning": "This field is a link field. Deleting this field will also delete the paired field. This action cannot be undone.", + "table.editProperty": "Edit Property", + "table.field.checkbox": "Checkbox", + "table.field.createdBy": "Created By", + "table.field.createdTime": "Created Time", + "table.field.date": "Date", + "table.field.deleteConfirmTitle": "Are you sure you want to delete this field?", + "table.field.deleteLinkFieldWarning": "This field is a link field. Deleting this field will also delete the paired field. This action cannot be undone.", + "table.field.file": "Files", + "table.field.formula": "Formula", + "table.field.lastEditedBy": "Last Edited By", + "table.field.lastEditedTime": "Last Edited Time", + "table.field.link": "Link", + "table.field.lookup": "Lookup", + "table.field.multiSelect": "Multi-select", + "table.field.noFieldFound": "No field found.", + "table.field.number": "Number", + "table.field.rating": "Rating", + "table.field.searchField": "Search field", + "table.field.select": "Select", + "table.field.selectField": "Select Field", + "table.field.text": "Text", + "table.field.untitledField": "Untitled Field", + "table.field.url": "URL", + "table.fieldType": "Type", + "table.sortAscending": "Sort Ascending", + "table.sortDescending": "Sort Descending", + "table.view.addFilter": "Add filter", + "table.view.addGroupFilter": "Add group filter", + "table.view.addSort": "Add sort", + "table.view.and": "AND", + "table.view.deleteConfirmDescription": "This action cannot be undone. This will permanently delete the view.", + "table.view.deleteConfirmTitle": "Are you sure you want to delete this view?", + "table.view.deleteFilter": "Delete filter", + "table.view.deleteSort": "Delete sort", + "table.view.disabledViewTypesWarning": "The disabled view types are not ready for large data", + "table.view.docList": "Doc list (beta)", + "table.view.edit": "Edit", + "table.view.field.addField": "Add Field", + "table.view.field.hideAll": "Hide all", + "table.view.field.showAll": "Show all", + "table.view.gallery": "Gallery", + "table.view.gallery.content": "Content", + "table.view.gallery.coverPreview": "Cover preview", + "table.view.gallery.hideEmptyFields": "Hide empty fields", + "table.view.grid": "Grid", + "table.view.noFilterRule": "There is no filter rule, add one", + "table.view.noSortRule": "There is no sort rule, add one", + "table.view.or": "OR", + "table.view.query": "Query", + "table.view.queryDescription": "SQL query to use for this view.", + "table.view.typeDescription": "The type of view to use for this table.", + "table.view.where": "Where" +} \ No newline at end of file diff --git a/locales/i18n.ts b/locales/i18n.ts new file mode 100644 index 00000000..332807a6 --- /dev/null +++ b/locales/i18n.ts @@ -0,0 +1,44 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import Backend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import enTranslations from './en.json'; +import zhTranslations from './zh.json'; + +const resources = { + en: { translation: enTranslations }, + zh: { translation: zhTranslations } +}; + +const getUserLanguage = () => { + const appearancePreferences = localStorage.getItem('appearancePreferences'); + if (appearancePreferences) { + const parsedPreferences = JSON.parse(appearancePreferences) + return parsedPreferences.language || 'en'; + } + return 'en'; +}; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources, + lng: getUserLanguage(), + fallbackLng: 'en', + debug: true, + interpolation: { + escapeValue: false, + }, + }).then(() => { + console.log('i18n initialized successfully'); + console.log('Current language:', i18n.language); + }).catch((err) => { + console.log('Error initializing i18n:', err); + }); + + + +export default i18n; diff --git a/locales/json.d.ts b/locales/json.d.ts new file mode 100644 index 00000000..09eae417 --- /dev/null +++ b/locales/json.d.ts @@ -0,0 +1,4 @@ +declare module "*.json" { + const value: any; + export default value; +} \ No newline at end of file diff --git a/locales/zh.json b/locales/zh.json new file mode 100644 index 00000000..ecfbed96 --- /dev/null +++ b/locales/zh.json @@ -0,0 +1,322 @@ +{ + "aiChat.inputEditor.pressAtToMentionResource": "@ 提及资源", + "aiChat.inputEditor.pressSlashToSwitchPrompt": "按 / 切换提示词", + "aiChat.inputEditor.typeYourMessageHere": "在此输入您的消息。", + "cmdk.inputPlaceholder": "输入命令或搜索...(输入 / 以查看脚本)", + "cmdk.newDraftDoc": "新建草稿文档", + "cmdk.notFound": "未找到 \"{{input}}\"", + "cmdk.suggestions": "建议", + "cmdk.switchTheme": "切换主题", + "common.ai": "AI", + "common.apiKey": "API 密钥", + "common.areYouAbsolutelySure": "您确定要这样做吗?", + "common.cancel": "取消", + "common.continue": "继续", + "common.delete": "删除", + "common.download": "下载", + "common.error": "错误", + "common.esc": "ESC", + "common.export": "导出", + "common.extensions": "扩展", + "common.fetch": "获取", + "common.files": "文件", + "common.filter": "过滤...", + "common.import": "导入", + "common.importFileDescription": "选择要导入到工作区的文件。", + "common.lock": "已锁定", + "common.name": "名称", + "common.new": "新建", + "common.noResultsFound": "未找到结果", + "common.noTableFound": "未找到表格。", + "common.nodes": "节点", + "common.open": "打开", + "common.paste": "粘贴", + "common.pinned": "置顶", + "common.remove": "移除", + "common.restore": "恢复", + "common.search": "搜索", + "common.select": "选择", + "common.settings": "设置", + "common.shareMode": "共享模式", + "common.system": "系统", + "common.test": "测试", + "common.thisActionCannotBeUndone": "此操作无法撤消。", + "common.today": "今天", + "common.trash": "回收站", + "common.update": "更新", + "doc.addCover": "添加封面", + "doc.addIcon": "添加图标", + "doc.changeCover": "更改封面", + "doc.coverImage": "封面图片", + "doc.hideProperties": "隐藏属性", + "doc.menu.background": "背景 {{name}}", + "doc.menu.bookmark": "书签", + "doc.menu.bulletedList": "无序列表", + "doc.menu.checkList": "检查列表", + "doc.menu.code": "代码", + "doc.menu.color": "颜色 {{name}}", + "doc.menu.databaseTable": "数据库表", + "doc.menu.divider": "分割线", + "doc.menu.heading": "标题 {{n}}", + "doc.menu.image": "图片", + "doc.menu.insertTable": "{{rows}}x{{columns}} 表格", + "doc.menu.numberedList": "有序列表", + "doc.menu.paragraph": "段落", + "doc.menu.query": "查询", + "doc.menu.quote": "引用", + "doc.menu.tableOfContent": "目录", + "doc.nodeInTrash": "此节点在回收站中", + "doc.permanentDeleteWarning": "此操作无法撤消。这将永久删除该节点", + "doc.pressForCommand": "按 / 键输入命令", + "doc.showProperties": "显示属性", + "doc.untitled": "无标题", + "everyday.import.importing": "正在导入...", + "everyday.import.selectFolder": "点击此处选择 Logseq 日记文件夹", + "everyday.import.title": "从 Logseq 导入(测试版)", + "kbd.shortcuts.common.description": "描述", + "kbd.shortcuts.common.openSettingsDescription": "打开设置菜单", + "kbd.shortcuts.common.shortcut": "快捷键", + "kbd.shortcuts.common.title": "常用键盘快捷键", + "kbd.shortcuts.common.toggleChatbotDescription": "打开或关闭聊天机器人界面", + "kbd.shortcuts.common.toggleCommandPaletteDescription": "打开或关闭命令面板", + "kbd.shortcuts.common.toggleSidebarDescription": "显示或隐藏侧边栏", + "kbd.shortcuts.common.toggleThemeDescription": "在亮色和暗色主题之间切换", + "kbd.shortcuts.document.checkboxDescription": "创建复选框", + "kbd.shortcuts.document.codeBlockDescription": "创建代码块", + "kbd.shortcuts.document.ctrlBDescription": "切换选中文本的粗体格式", + "kbd.shortcuts.document.ctrlIDescription": "切换选中文本的斜体格式", + "kbd.shortcuts.document.ctrlSDescription": "保存当前文档", + "kbd.shortcuts.document.ctrlUDescription": "切换选中文本的下划线格式", + "kbd.shortcuts.document.heading1Description": "创建一级标题", + "kbd.shortcuts.document.heading2Description": "创建二级标题", + "kbd.shortcuts.document.heading3Description": "创建三级标题", + "kbd.shortcuts.document.horizontalRuleDescription": "插入水平分割线", + "kbd.shortcuts.document.orderedListDescription": "创建有序列表项", + "kbd.shortcuts.document.title": "文档键盘快捷键", + "kbd.shortcuts.document.unorderedListDescription": "创建无序列表项", + "kbd.shortcuts.table.altArrowDescription": "移动当前选中的单元格并保留当前选择", + "kbd.shortcuts.table.arrowDescription": "移动当前选中的单元格并清除其他选择", + "kbd.shortcuts.table.ctrlADescription": "选择整个表格", + "kbd.shortcuts.table.ctrlArrowDescription": "移动到当前数据区域在按下方向上的边缘", + "kbd.shortcuts.table.ctrlCDescription": "复制选中的单元格", + "kbd.shortcuts.table.ctrlDDescription": "用最上方选中单元格的值填充选中的单元格", + "kbd.shortcuts.table.ctrlFDescription": "打开搜索对话框", + "kbd.shortcuts.table.ctrlHomeEndDescription": "移动到表格的第一个或最后一个单元格", + "kbd.shortcuts.table.ctrlRDescription": "用最左侧选中单元格的值填充选中的单元格", + "kbd.shortcuts.table.ctrlShiftArrowDescription": "将当前选择扩展到当前数据区域在按下方向上的边缘", + "kbd.shortcuts.table.ctrlShiftHomeEndDescription": "从当前单元格选择到表格的第一个或最后一个单元格", + "kbd.shortcuts.table.ctrlSpaceDescription": "选择整列", + "kbd.shortcuts.table.ctrlVDescription": "粘贴复制的单元格", + "kbd.shortcuts.table.escapeDescription": "取消选择所有选中的单元格", + "kbd.shortcuts.table.pageUpDownDescription": "上下翻页", + "kbd.shortcuts.table.shiftArrowDescription": "向按下的方向扩展当前选择范围", + "kbd.shortcuts.table.shiftHomeEndDescription": "将选择扩展到当前行的开始或结束", + "kbd.shortcuts.table.shiftSpaceDescription": "选择整行", + "kbd.shortcuts.table.title": "表格(网格视图)键盘快捷键", + "nav.dropdown.menu.commandPalette": "命令面板", + "nav.dropdown.menu.creatingEmbedding": "正在为 {{name}} 创建嵌入", + "nav.dropdown.menu.desktop": "桌面版", + "nav.dropdown.menu.emailInstructions": "1. 扫描二维码将地址添加到您的联系人\n2. 发送邮件到此地址以将数据保存到此表格", + "nav.dropdown.menu.embedding": "嵌入(测试版)", + "nav.dropdown.menu.embeddingCreated": "嵌入已创建", + "nav.dropdown.menu.fullWidth": "全宽", + "nav.dropdown.menu.keyboardShortcuts": "键盘快捷键", + "nav.dropdown.menu.lastUpdated": "最后更新:{{time}}", + "nav.dropdown.menu.lock": "锁定", + "nav.dropdown.menu.mail": "邮件", + "nav.dropdown.menu.sendMailToEidos": "发送邮件到 Eidos", + "nav.dropdown.menu.version": "版本:{{version}}({{mode}})", + "nav.dropdown.menu.web": "网页版", + "nav.dropdown.menu.website": "网站首页", + "nav.status.apiAgentConnected": "API 代理已连接", + "nav.status.checkForUpdates": "检查更新", + "nav.status.clickToPin": "点击固定此节点", + "nav.status.clickToUnpin": "点击取消固定此节点", + "nav.status.noApiAgentConnected": "未连接 API 代理", + "nav.status.noUpdatesAvailable": "无可用更新", + "nav.status.nodeLocked": "此节点已锁定(只读)", + "nav.status.updateAvailable": "更新到 v{{version}}", + "node.menu.cancelCut": "取消剪切", + "node.menu.cut": "剪切", + "node.menu.duplicate": "复制", + "node.menu.moveInto": "移动到", + "node.menu.newDoc": "新建文档", + "node.menu.newFolder": "新建文件夹", + "node.menu.newNestedFolder": "新建嵌套文件夹", + "node.menu.newTable": "新建表格", + "node.menu.pin": "固定", + "node.menu.rename": "重命名", + "node.menu.unpin": "取消固定", + "settings.ai": "AI", + "settings.ai.addProvider": "添加提供者", + "settings.ai.addProviderDescription": "将新的 LLM 提供者添加到您的配置中。", + "settings.ai.apiKeyDescription": "输入您的 API 密钥以进行身份验证。", + "settings.ai.baseUrl": "基础 URL", + "settings.ai.baseUrlDescription": "API 的基础 URL。", + "settings.ai.baseUrlRequired": "基础 URL 是必需的。", + "settings.ai.codingModel": "编码模型", + "settings.ai.codingModelDescription": "选择您偏好的编码任务模型", + "settings.ai.configUpdated": "AI 配置已更新。", + "settings.ai.description": "配置您的 AI 设置。", + "settings.ai.embeddingModel": "嵌入模型", + "settings.ai.embeddingModelDescription": "选择您偏好的嵌入任务模型", + "settings.ai.fetchModelListError": "获取模型列表失败。", + "settings.ai.localLLMDescription": "管理您的本地 LLM。", + "settings.ai.localLLMTitle": "本地 LLM", + "settings.ai.modelPreferences": "模型偏好", + "settings.ai.modelPreferencesDescription": "为不同任务选择偏好的模型", + "settings.ai.models": "模型", + "settings.ai.modelsDescription": "以逗号分隔的模型 ID 列表。", + "settings.ai.provider": "提供者", + "settings.ai.providerDescription": "有许多 LLM API 提供者。根据您的需要进行配置。", + "settings.ai.providerType": "提供者类型", + "settings.ai.translationModel": "翻译模型", + "settings.ai.translationModelDescription": "选择您偏好的翻译任务模型", + "settings.ai.updateLLMProvider": "更新 LLM 提供者", + "settings.api": "API", + "settings.api.agentUrl": "API 代理 URL", + "settings.api.agentUrlDescription": "您的 API 代理的 URL。", + "settings.api.callApiThrough": "通过以下方式调用 API", + "settings.api.description": "配置您的 API 设置。", + "settings.api.enable": "启用", + "settings.api.enableDescription": "启用后,您可以通过 API 代理从 Eidos Web APP 查询数据", + "settings.api.regenerate": "重新生成", + "settings.api.update": "更新", + "settings.appearance": "外观", + "settings.appearance.dark": "暗色", + "settings.appearance.description": "自定义应用的外观。自动在亮色和暗色主题之间切换。", + "settings.appearance.font": "字体", + "settings.appearance.fontDescription": "设置您想在仪表板中使用的字体。", + "settings.appearance.language": "语言", + "settings.appearance.languageDescription": "选择您偏好的语言。", + "settings.appearance.light": "亮色", + "settings.appearance.submittedValues": "您提交了以下值:", + "settings.appearance.theme": "主题", + "settings.appearance.themeDescription": "选择您偏好的主题。", + "settings.appearance.title": "外观", + "settings.appearance.updatePreferences": "更新偏好设置", + "settings.devtools": "开发工具", + "settings.devtools.description": "开发者工具设置。", + "settings.experiment": "实验", + "settings.experiment.description": "试验性功能设置。", + "settings.general": "通用", + "settings.general.clientId": "客户端 ID", + "settings.general.description": "他人在协作时如何看到您。", + "settings.manageAppSettings": "管理应用设置和配置", + "settings.security": "安全", + "settings.security.description": "配置您的安全设置。", + "settings.storage": "存储", + "settings.storage.autoBackupDescription": "每 {{minutes}} 分钟备份一次数据,0 表示禁用自动保存。", + "settings.storage.autoBackupDisabled": "禁用自动保存。", + "settings.storage.autoBackupExplanation": "将每个空间的数据库备份到本地路径。保持数据更安全。", + "settings.storage.autoBackupGap": "自动备份间隔(分钟)", + "settings.storage.dataFolder": "数据文件夹", + "settings.storage.dataFolderDescription": "您的数据将存储在此文件夹中。", + "settings.storage.dataFolderNotSelected": "未选择数据文件夹", + "settings.storage.description": "配置您的存储设置。", + "settings.storage.fileSystem": "文件系统", + "settings.storage.fileSystemDescription": "选择用于存储文件的文件系统。OPFS 将文件存储在浏览器的存储中,而本地文件系统将文件存储在您设备上的地目录中。", + "settings.storage.grantPermission": "授予权限", + "settings.storage.nativeFileSystem": "本地文件系统", + "settings.storage.noTypeFound": "未找到类型。", + "settings.storage.permissionDenied": "权限被拒绝。", + "settings.storage.permissionGranted": "已授予权限。", + "settings.storage.searchType": "搜索类型...", + "settings.storage.selectDataFolder": "您需要选择一个数据文件夹。", + "settings.storage.selectDataFolderPlaceholder": "选择数据文件夹", + "settings.storage.selectFileSystem": "选择文件系统", + "settings.storage.settingsUpdated": "设置已更新", + "settings.sync": "同步", + "settings.sync.description": "配置您的同步设置。", + "settings.title": "设置", + "sidebar.importFile.description": "选择要导入到工作区的文件。", + "sidebar.importFile.importCSV": "导入 CSV", + "sidebar.importFile.importCSVDescription": "从 CSV 文件导入表格数据。", + "sidebar.importFile.importMarkdown": "导入 Markdown", + "sidebar.importFile.importMarkdownDescription": "从 Markdown 文件导入文档。", + "sidebar.importFile.title": "导入文件", + "sidebar.trash.restoreOrPermanentlyDeleteNodes": "恢复或永久删除节点", + "sidebar.trash.thisActionCannotBeUndone": "此操作无法撤消。这将永久删除所选节点。", + "sidebar.trash.trashIsEmpty": "回收站为空", + "space.select.createNew": "创建新数据库", + "space.select.createSpace": "创建空间", + "space.select.createSpaceDescription": "添加一个新空间以管理数据", + "space.select.creating": "正在创建", + "space.select.importFromFile": "从文件导入", + "space.select.importFromFileDescription": "如果您将空间导出为 zip 文件,您可以在此处导入它", + "space.select.overwriteWarning": "似乎您正在尝试覆盖现有空间。请小心,这将覆盖现有空间中的数据。", + "space.select.searchDatabase": "搜索数据库...", + "space.select.selectDatabase": "选择数据库...", + "space.select.spaceAlreadyExists": "此空间已存在,请选择另一个名称", + "space.select.spaceName": "空间名称", + "space.select.spaceNamePlaceholder": "例如:personal", + "space.settings.dangerZone": "危险区域", + "space.settings.deleteSpace": "删除空间", + "space.settings.deleteSpaceDescription": "永久删除此空间及其所有内容。此操作无法撤消。", + "space.settings.deleteSpaceWarning": "此操作无法撤消。这将永久删除您的空间 <1>{{spaceName}}。请输入空间名称以确认。", + "space.settings.description": "这些设置仅适用于当前空间。如果您想更改所有空间的设置,请前往", + "space.settings.exportDescription": "导出此空间的所有数据以进行备份或传输。", + "space.settings.exportSpace": "导出空间", + "space.settings.globalSettings": "全局设置", + "space.settings.rebuildIndex": "重建索引", + "space.settings.rebuildIndexDescription": "重新构建此空间的搜索索引。如果您遇到搜索问题,请使用此功能。", + "space.settings.rebuilding": "正在重建...", + "space.settings.title": "空间设置", + "space.settings.typeSpaceName": "输入空间名称", + "table.deleteField": "删除字段", + "table.deleteFieldConfirmation": "您确定要删除此字段吗?", + "table.deleteLinkFieldWarning": "此字段是链接字段。删除此字段也会删除配对字段。此操作无法撤消。", + "table.editProperty": "编辑属性", + "table.field.checkbox": "复选框", + "table.field.createdBy": "创建者", + "table.field.createdTime": "创建时间", + "table.field.date": "日期", + "table.field.deleteConfirmTitle": "您确定要删除此字段吗?", + "table.field.deleteLinkFieldWarning": "此字段是链接字段。删除此字段也会删除配对字段。此操作无法撤消。", + "table.field.file": "文件", + "table.field.formula": "公式", + "table.field.lastEditedBy": "最后编辑者", + "table.field.lastEditedTime": "最后编辑时间", + "table.field.link": "关联", + "table.field.lookup": "查找", + "table.field.multiSelect": "多选", + "table.field.noFieldFound": "未找到字段。", + "table.field.number": "数字", + "table.field.rating": "评分", + "table.field.searchField": "搜索字段", + "table.field.select": "单选", + "table.field.selectField": "选择字段", + "table.field.text": "文本", + "table.field.title": "标题", + "table.field.untitledField": "未命名字段", + "table.field.url": "网址", + "table.fieldType": "类型", + "table.sortAscending": "升序", + "table.sortDescending": "降序", + "table.view.addFilter": "添加筛选", + "table.view.addGroupFilter": "添加分组筛选", + "table.view.addSort": "添加排序", + "table.view.and": "并且", + "table.view.deleteConfirmDescription": "此操作无法撤消。这将永久删除该视图。", + "table.view.deleteConfirmTitle": "您确定要删除此视图吗?", + "table.view.deleteFilter": "删除筛选", + "table.view.deleteSort": "删除排序", + "table.view.disabledViewTypesWarning": "禁用的视图类型不适用于大量数据", + "table.view.docList": "文档列表(测试版)", + "table.view.edit": "编辑", + "table.view.field.addField": "添加字段", + "table.view.field.hideAll": "隐藏全部", + "table.view.field.showAll": "显示全部", + "table.view.gallery": "画廊", + "table.view.gallery.content": "内容", + "table.view.gallery.coverPreview": "封面预览", + "table.view.gallery.hideEmptyFields": "隐藏空字段", + "table.view.grid": "网格", + "table.view.noFilterRule": "没有筛选规则,添加一个", + "table.view.noSortRule": "没有排序规则,添加一个", + "table.view.or": "或者", + "table.view.query": "查询", + "table.view.queryDescription": "用于此视图的 SQL 查询。", + "table.view.typeDescription": "用于此表格的视图类型。", + "table.view.where": "当" +} \ No newline at end of file diff --git a/package.json b/package.json index 40e403e3..dd3e2f2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eidos", - "version": "0.7.9", + "version": "0.8.0", "private": true, "type": "module", "description": "Eidos is an extensible framework for managing your personal data throughout your lifetime in one place.", @@ -175,6 +175,9 @@ "hnswlib-wasm": "^0.8.2", "hono": "^4.5.11", "html2canvas": "^1.4.1", + "i18next": "^23.16.2", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.6.2", "idb-keyval": "^6.2.1", "immutability-helper": "^3.1.1", "jszip": "^3.10.1", @@ -204,6 +207,7 @@ "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-hook-form": "^7.45.1", + "react-i18next": "^15.1.0", "react-infinite-scroll-hook": "^4.1.1", "react-laag": "^2.0.5", "react-marked-renderer": "^1.1.2", @@ -299,4 +303,4 @@ ] } } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a01ac19d..a52d3ff4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -263,6 +263,15 @@ importers: html2canvas: specifier: ^1.4.1 version: 1.4.1 + i18next: + specifier: ^23.16.2 + version: 23.16.2 + i18next-browser-languagedetector: + specifier: ^8.0.0 + version: 8.0.0 + i18next-http-backend: + specifier: ^2.6.2 + version: 2.6.2 idb-keyval: specifier: ^6.2.1 version: 6.2.1 @@ -350,6 +359,9 @@ importers: react-hook-form: specifier: ^7.45.1 version: 7.45.1(react@18.2.0) + react-i18next: + specifier: ^15.1.0 + version: 15.1.0(i18next@23.16.2)(react-dom@18.2.0)(react@18.2.0) react-infinite-scroll-hook: specifier: ^4.1.1 version: 4.1.1(react@18.2.0) @@ -4546,13 +4558,13 @@ packages: /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 dev: false /@radix-ui/primitive@1.0.0: resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==} dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 dev: false /@radix-ui/primitive@1.0.1: @@ -4633,7 +4645,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.14 '@types/react-dom': 18.2.6 @@ -4798,7 +4810,7 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 react: 18.2.0 dev: false @@ -4860,7 +4872,7 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 react: 18.2.0 dev: false @@ -4897,7 +4909,7 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/primitive': 1.0.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) '@radix-ui/react-context': 1.0.0(react@18.2.0) @@ -4972,7 +4984,7 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/primitive': 1.0.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) '@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0) @@ -4995,7 +5007,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) @@ -5020,7 +5032,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) @@ -5064,7 +5076,7 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 react: 18.2.0 dev: false @@ -5077,7 +5089,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@types/react': 18.2.14 react: 18.2.0 dev: false @@ -5088,7 +5100,7 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) '@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0) @@ -5109,7 +5121,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.14)(react@18.2.0) @@ -5132,7 +5144,7 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0) react: 18.2.0 dev: false @@ -5200,7 +5212,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0) @@ -5273,7 +5285,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@floating-ui/react-dom': 2.0.1(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0) @@ -5303,7 +5315,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@floating-ui/react-dom': 2.0.1(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0) @@ -5326,7 +5338,7 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5345,7 +5357,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.14 '@types/react-dom': 18.2.6 @@ -5366,7 +5378,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.14 '@types/react-dom': 18.2.6 @@ -5380,7 +5392,7 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0) react: 18.2.0 @@ -5436,7 +5448,7 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-slot': 1.0.0(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5548,7 +5560,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0) @@ -5691,7 +5703,7 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) react: 18.2.0 dev: false @@ -5872,7 +5884,7 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 react: 18.2.0 dev: false @@ -5908,7 +5920,7 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0) react: 18.2.0 dev: false @@ -5947,7 +5959,7 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0) react: 18.2.0 dev: false @@ -5961,7 +5973,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.14)(react@18.2.0) '@types/react': 18.2.14 react: 18.2.0 @@ -5972,7 +5984,7 @@ packages: peerDependencies: react: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 react: 18.2.0 dev: false @@ -6026,7 +6038,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/rect': 1.0.1 '@types/react': 18.2.14 react: 18.2.0 @@ -6060,7 +6072,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.14 '@types/react-dom': 18.2.6 @@ -6071,7 +6083,7 @@ packages: /@radix-ui/rect@1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 dev: false /@react-dnd/asap@5.0.2: @@ -8615,6 +8627,14 @@ packages: cross-spawn: 7.0.3 dev: true + /cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + dependencies: + node-fetch: 2.6.13 + transitivePeerDependencies: + - encoding + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -10731,6 +10751,12 @@ packages: whatwg-encoding: 3.1.1 dev: true + /html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + dependencies: + void-elements: 3.1.0 + dev: false + /html-rewriter-wasm@0.4.1: resolution: {integrity: sha512-lNovG8CMCCmcVB1Q7xggMSf7tqPCijZXaH4gL6iE8BFghdQCbaY5Met9i1x2Ex8m/cZHDUtXK9H6/znKamRP8Q==} dev: true @@ -10823,6 +10849,26 @@ packages: ms: 2.1.3 dev: false + /i18next-browser-languagedetector@8.0.0: + resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==} + dependencies: + '@babel/runtime': 7.25.7 + dev: false + + /i18next-http-backend@2.6.2: + resolution: {integrity: sha512-Hp/kd8/VuoxIHmxsknJXjkTYYHzivAyAF15pzliKzk2TiXC25rZCEerb1pUFoxz4IVrG3fCvQSY51/Lu4ECV4A==} + dependencies: + cross-fetch: 4.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /i18next@23.16.2: + resolution: {integrity: sha512-dFyxwLXxEQK32f6tITBMaRht25mZPJhQ0WbC0p3bO2mWBal9lABTMqSka5k+GLSRWLzeJBKDpH7BeIA9TZI7Jg==} + dependencies: + '@babel/runtime': 7.25.7 + dev: false + /iconv-corefoundation@1.1.7: resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} engines: {node: ^8.11.2 || >=10} @@ -13095,6 +13141,26 @@ packages: html-element-attributes: 1.3.1 dev: false + /react-i18next@15.1.0(i18next@23.16.2)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-zj3nJynMnZsy2gPZiOTC7XctCY5eQGqT3tcKMmfJWC9FMvgd+960w/adq61j8iPzpwmsXejqID9qC3Mqu1Xu2Q==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.25.7 + html-parse-stringify: 3.0.1 + i18next: 23.16.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-infinite-scroll-hook@4.1.1(react@18.2.0): resolution: {integrity: sha512-1bu2572rF3DtjFMhIOzoasLMdYW0vMWxROtl99M5FYGSxm84Ro4aNBZW6ivgE45ofus4Ymo7jIS0Be3zcuLk8g==} engines: {node: '>=10'} @@ -13373,7 +13439,7 @@ packages: /redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.7 dev: false /regenerate-unicode-properties@10.2.0: @@ -15194,6 +15260,11 @@ packages: - terser dev: true + /void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + dev: false + /w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'}