From d477c7270c90b190ed82b13f48f39a05d83503d2 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 21 Sep 2023 23:49:12 +0800 Subject: Fix #1394. --- FrontEnd/src/common.ts | 9 - FrontEnd/src/components/AppBar.tsx | 3 +- FrontEnd/src/components/SearchInput.tsx | 4 +- FrontEnd/src/components/alert/AlertHost.tsx | 4 +- FrontEnd/src/components/alert/AlertService.ts | 4 +- FrontEnd/src/components/button/Button.tsx | 4 +- FrontEnd/src/components/button/ButtonRowV2.tsx | 10 +- FrontEnd/src/components/button/FlatButton.tsx | 4 +- FrontEnd/src/components/common.ts | 9 +- FrontEnd/src/components/dialog/ConfirmDialog.tsx | 6 +- FrontEnd/src/components/dialog/DialogContainer.tsx | 4 +- FrontEnd/src/components/dialog/OperationDialog.tsx | 14 +- FrontEnd/src/components/hooks/useWindowLeave.ts | 4 +- FrontEnd/src/components/input/InputGroup.tsx | 14 +- FrontEnd/src/components/menu/Menu.tsx | 4 +- FrontEnd/src/components/tab/TabBar.tsx | 4 +- FrontEnd/src/components/tab/TabPages.tsx | 4 +- FrontEnd/src/i18n.ts | 114 ---------- FrontEnd/src/i18n/backend.ts | 33 +++ FrontEnd/src/i18n/index.ts | 3 + FrontEnd/src/i18n/setup.ts | 50 +++++ FrontEnd/src/i18n/text.ts | 35 +++ FrontEnd/src/i18n/translations/en/admin.json | 35 +++ FrontEnd/src/i18n/translations/en/index.json | 228 +++++++++++++++++++ FrontEnd/src/i18n/translations/zh/admin.json | 35 +++ FrontEnd/src/i18n/translations/zh/index.json | 241 +++++++++++++++++++++ FrontEnd/src/locales/en/admin.json | 35 --- FrontEnd/src/locales/en/translation.json | 228 ------------------- FrontEnd/src/locales/zh/admin.json | 35 --- FrontEnd/src/locales/zh/translation.json | 241 --------------------- .../hooks/useReverseScrollPositionRemember.ts | 58 +++++ FrontEnd/src/pages/about/index.tsx | 2 +- FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 5 +- FrontEnd/src/pages/setting/index.tsx | 15 +- FrontEnd/src/pages/timeline/Timeline.tsx | 2 +- FrontEnd/src/pages/timeline/TimelineMember.tsx | 3 +- .../pages/timeline/edit/TimelinePostCreateView.tsx | 4 +- FrontEnd/src/pages/timeline/index.tsx | 2 +- FrontEnd/src/services/alert.ts | 3 +- FrontEnd/src/services/user.ts | 2 +- FrontEnd/src/utilities/hooks/use-c.ts | 7 - .../hooks/useReverseScrollPositionRemember.ts | 58 ----- 42 files changed, 784 insertions(+), 795 deletions(-) delete mode 100644 FrontEnd/src/common.ts delete mode 100644 FrontEnd/src/i18n.ts create mode 100644 FrontEnd/src/i18n/backend.ts create mode 100644 FrontEnd/src/i18n/index.ts create mode 100644 FrontEnd/src/i18n/setup.ts create mode 100644 FrontEnd/src/i18n/text.ts create mode 100644 FrontEnd/src/i18n/translations/en/admin.json create mode 100644 FrontEnd/src/i18n/translations/en/index.json create mode 100644 FrontEnd/src/i18n/translations/zh/admin.json create mode 100644 FrontEnd/src/i18n/translations/zh/index.json delete mode 100644 FrontEnd/src/locales/en/admin.json delete mode 100644 FrontEnd/src/locales/en/translation.json delete mode 100644 FrontEnd/src/locales/zh/admin.json delete mode 100644 FrontEnd/src/locales/zh/translation.json create mode 100644 FrontEnd/src/migrating/hooks/useReverseScrollPositionRemember.ts delete mode 100644 FrontEnd/src/utilities/hooks/use-c.ts delete mode 100644 FrontEnd/src/utilities/hooks/useReverseScrollPositionRemember.ts diff --git a/FrontEnd/src/common.ts b/FrontEnd/src/common.ts deleted file mode 100644 index 1ca796c3..00000000 --- a/FrontEnd/src/common.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This error is thrown when ui goes wrong with bad logic. -// Such as a variable should not be null, but it does. -// This error should never occur. If it does, it indicates there is some logic bug in codes. -export class UiLogicError extends Error {} - -export type { I18nText } from "./i18n"; -export type { I18nText as Text } from "./i18n"; -export { c, convertI18nText } from "./i18n"; -export { default as useC } from "./utilities/hooks/use-c"; diff --git a/FrontEnd/src/components/AppBar.tsx b/FrontEnd/src/components/AppBar.tsx index d40c8105..6ea8bdac 100644 --- a/FrontEnd/src/components/AppBar.tsx +++ b/FrontEnd/src/components/AppBar.tsx @@ -37,7 +37,8 @@ function AppBarNavLink({ className={({ isActive }) => classnames(className, isActive && "active")} onClick={onClick} > - {children != null ? children : c(label)} + {children} + {label && c(label)} ); } diff --git a/FrontEnd/src/components/SearchInput.tsx b/FrontEnd/src/components/SearchInput.tsx index b1de6227..04341245 100644 --- a/FrontEnd/src/components/SearchInput.tsx +++ b/FrontEnd/src/components/SearchInput.tsx @@ -1,6 +1,6 @@ import classNames from "classnames"; -import { useC, Text } from "./common"; +import { useC, I18nText } from "./common"; import { LoadingButton } from "./button"; import "./SearchInput.css"; @@ -11,7 +11,7 @@ interface SearchInputProps { onButtonClick: () => void; loading?: boolean; className?: string; - buttonText?: Text; + buttonText?: I18nText; } export default function SearchInput({ diff --git a/FrontEnd/src/components/alert/AlertHost.tsx b/FrontEnd/src/components/alert/AlertHost.tsx index 59f8f27c..8dca42d5 100644 --- a/FrontEnd/src/components/alert/AlertHost.tsx +++ b/FrontEnd/src/components/alert/AlertHost.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import classNames from "classnames"; -import { ThemeColor, useC, Text } from "../common"; +import { ThemeColor, useC, I18nText } from "../common"; import IconButton from "../button/IconButton"; import { alertService, AlertInfoWithId } from "./AlertService"; @@ -10,7 +10,7 @@ import "./alert.css"; interface AutoCloseAlertProps { color: ThemeColor; - message: Text; + message: I18nText; onDismiss?: () => void; onIn?: () => void; onOut?: () => void; diff --git a/FrontEnd/src/components/alert/AlertService.ts b/FrontEnd/src/components/alert/AlertService.ts index b9cda752..8e98cc4d 100644 --- a/FrontEnd/src/components/alert/AlertService.ts +++ b/FrontEnd/src/components/alert/AlertService.ts @@ -1,10 +1,10 @@ -import { ThemeColor, Text } from "../common"; +import { ThemeColor, I18nText } from "../common"; const defaultDismissTime = 5000; export interface AlertInfo { color?: ThemeColor; - message: Text; + message: I18nText; dismissTime?: number | "never"; } diff --git a/FrontEnd/src/components/button/Button.tsx b/FrontEnd/src/components/button/Button.tsx index 30ea8c11..bdb7bb2d 100644 --- a/FrontEnd/src/components/button/Button.tsx +++ b/FrontEnd/src/components/button/Button.tsx @@ -1,13 +1,13 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; -import { Text, useC, ClickableColor } from "../common"; +import { I18nText, useC, ClickableColor } from "../common"; import "./Button.css"; interface ButtonProps extends ComponentPropsWithoutRef<"button"> { color?: ClickableColor; - text?: Text; + text?: I18nText; outline?: boolean; buttonRef?: Ref | null; } diff --git a/FrontEnd/src/components/button/ButtonRowV2.tsx b/FrontEnd/src/components/button/ButtonRowV2.tsx index a54425cc..75f2ad9d 100644 --- a/FrontEnd/src/components/button/ButtonRowV2.tsx +++ b/FrontEnd/src/components/button/ButtonRowV2.tsx @@ -1,7 +1,7 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; -import { Text, ClickableColor } from "../common"; +import { I18nText, ClickableColor } from "../common"; import Button from "./Button"; import FlatButton from "./FlatButton"; @@ -22,21 +22,21 @@ interface ButtonRowV2ButtonBase { interface ButtonRowV2ButtonWithNoType extends ButtonRowV2ButtonBase { type?: undefined | null; - text: Text; + text: I18nText; outline?: boolean; props?: ComponentPropsWithoutRef; } interface ButtonRowV2NormalButton extends ButtonRowV2ButtonBase { type: "normal"; - text: Text; + text: I18nText; outline?: boolean; props?: ComponentPropsWithoutRef; } interface ButtonRowV2FlatButton extends ButtonRowV2ButtonBase { type: "flat"; - text: Text; + text: I18nText; props?: ComponentPropsWithoutRef; } @@ -48,7 +48,7 @@ interface ButtonRowV2IconButton extends ButtonRowV2ButtonBase { interface ButtonRowV2LoadingButton extends ButtonRowV2ButtonBase { type: "loading"; - text: Text; + text: I18nText; loading?: boolean; props?: ComponentPropsWithoutRef; } diff --git a/FrontEnd/src/components/button/FlatButton.tsx b/FrontEnd/src/components/button/FlatButton.tsx index aad02e76..e15b8c2b 100644 --- a/FrontEnd/src/components/button/FlatButton.tsx +++ b/FrontEnd/src/components/button/FlatButton.tsx @@ -1,13 +1,13 @@ import { ComponentPropsWithoutRef, Ref } from "react"; import classNames from "classnames"; -import { Text, useC, ClickableColor } from "../common"; +import { I18nText, useC, ClickableColor } from "../common"; import "./FlatButton.css"; interface FlatButtonProps extends ComponentPropsWithoutRef<"button"> { color?: ClickableColor; - text?: Text; + text?: I18nText; buttonRef?: Ref | null; } diff --git a/FrontEnd/src/components/common.ts b/FrontEnd/src/components/common.ts index a6c3e705..840e1be5 100644 --- a/FrontEnd/src/components/common.ts +++ b/FrontEnd/src/components/common.ts @@ -1,7 +1,9 @@ import "./index.css"; -export type { Text, I18nText } from "~src/common"; -export { UiLogicError, c, convertI18nText, useC } from "~src/common"; +export type { I18nText } from "~src/i18n"; +export { convertI18nText, useC } from "~src/i18n"; + +export class UiLogicError extends Error {} export const themeColors = [ "primary", @@ -18,5 +20,4 @@ export { breakpoints } from "./breakpoints"; export * as geometry from "~src/utilities/geometry"; -export * as array from "~src/utilities/array" - +export * as array from "~src/utilities/array"; diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.tsx b/FrontEnd/src/components/dialog/ConfirmDialog.tsx index 4ee0ec03..199eee6b 100644 --- a/FrontEnd/src/components/dialog/ConfirmDialog.tsx +++ b/FrontEnd/src/components/dialog/ConfirmDialog.tsx @@ -1,4 +1,4 @@ -import { useC, Text, ThemeColor } from "../common"; +import { useC, I18nText, ThemeColor } from "../common"; import Dialog from "./Dialog"; import DialogContainer from "./DialogContainer"; @@ -14,8 +14,8 @@ export default function ConfirmDialog({ open: boolean; onClose: () => void; onConfirm: () => void; - title: Text; - body: Text; + title: I18nText; + body: I18nText; color?: ThemeColor; bodyColor?: ThemeColor; }) { diff --git a/FrontEnd/src/components/dialog/DialogContainer.tsx b/FrontEnd/src/components/dialog/DialogContainer.tsx index 6ee4e134..844d8ddd 100644 --- a/FrontEnd/src/components/dialog/DialogContainer.tsx +++ b/FrontEnd/src/components/dialog/DialogContainer.tsx @@ -1,14 +1,14 @@ import { ComponentProps, Ref, ReactNode } from "react"; import classNames from "classnames"; -import { ThemeColor, Text, useC } from "../common"; +import { ThemeColor, I18nText, useC } from "../common"; import { ButtonRow, ButtonRowV2 } from "../button"; import "./DialogContainer.css"; interface DialogContainerBaseProps { className?: string; - title: Text; + title: I18nText; titleColor?: ThemeColor; titleClassName?: string; titleRef?: Ref; diff --git a/FrontEnd/src/components/dialog/OperationDialog.tsx b/FrontEnd/src/components/dialog/OperationDialog.tsx index feaf5c79..4541d6f8 100644 --- a/FrontEnd/src/components/dialog/OperationDialog.tsx +++ b/FrontEnd/src/components/dialog/OperationDialog.tsx @@ -1,7 +1,7 @@ import { useState, ReactNode, ComponentProps } from "react"; import classNames from "classnames"; -import { useC, Text, ThemeColor } from "../common"; +import { useC, I18nText, ThemeColor } from "../common"; import { useInputs, InputGroup, @@ -15,8 +15,8 @@ import DialogContainer from "./DialogContainer"; import "./OperationDialog.css"; interface OperationDialogPromptProps { - message?: Text; - customMessage?: Text; + message?: I18nText; + customMessage?: I18nText; customMessageNode?: ReactNode; className?: string; } @@ -39,12 +39,12 @@ export interface OperationDialogProps { onClose: () => void; color?: ThemeColor; inputColor?: ThemeColor; - title: Text; - inputPrompt?: Text; + title: I18nText; + inputPrompt?: I18nText; inputPromptNode?: ReactNode; - successPrompt?: (data: TData) => Text; + successPrompt?: (data: TData) => I18nText; successPromptNode?: (data: TData) => ReactNode; - failurePrompt?: (error: unknown) => Text; + failurePrompt?: (error: unknown) => I18nText; failurePromptNode?: (error: unknown) => ReactNode; inputs: InputInitializer; diff --git a/FrontEnd/src/components/hooks/useWindowLeave.ts b/FrontEnd/src/components/hooks/useWindowLeave.ts index ecd999d4..b829a92f 100644 --- a/FrontEnd/src/components/hooks/useWindowLeave.ts +++ b/FrontEnd/src/components/hooks/useWindowLeave.ts @@ -1,10 +1,10 @@ import { useEffect } from "react"; -import { useC, Text } from "../common"; +import { useC, I18nText } from "../common"; export default function useWindowLeave( allow: boolean, - message: Text = "timeline.confirmLeave", + message: I18nText = "timeline.confirmLeave", ) { const c = useC(); diff --git a/FrontEnd/src/components/input/InputGroup.tsx b/FrontEnd/src/components/input/InputGroup.tsx index 47a43b38..be6cd577 100644 --- a/FrontEnd/src/components/input/InputGroup.tsx +++ b/FrontEnd/src/components/input/InputGroup.tsx @@ -26,16 +26,16 @@ import { useState, Ref, useId } from "react"; import classNames from "classnames"; -import { useC, Text, ThemeColor } from "../common"; +import { useC, I18nText, ThemeColor } from "../common"; import "./InputGroup.css"; export interface InputBase { key: string; - label: Text; - helper?: Text; + label: I18nText; + helper?: I18nText; disabled?: boolean; - error?: Text; + error?: I18nText; } export interface TextInput extends InputBase { @@ -51,7 +51,7 @@ export interface BoolInput extends InputBase { export interface SelectInputOption { value: string; - label: Text; + label: I18nText; icon?: string; } @@ -66,14 +66,14 @@ export type Input = TextInput | BoolInput | SelectInput; export type InputValue = Input["value"]; export type InputValueDict = Record; -export type InputErrorDict = Record; +export type InputErrorDict = Record; export type InputDisabledDict = Record; export type InputDirtyDict = Record; // use never so you don't have to cast everywhere export type InputConfirmValueDict = Record; export type GeneralInputErrorDict = { - [key: string]: Text | null | undefined; + [key: string]: I18nText | null | undefined; }; type MakeInputInfo = Omit; diff --git a/FrontEnd/src/components/menu/Menu.tsx b/FrontEnd/src/components/menu/Menu.tsx index 1a196a69..6093a56f 100644 --- a/FrontEnd/src/components/menu/Menu.tsx +++ b/FrontEnd/src/components/menu/Menu.tsx @@ -1,7 +1,7 @@ import { MouseEvent, CSSProperties } from "react"; import classNames from "classnames"; -import { useC, Text, ThemeColor } from "../common"; +import { useC, I18nText, ThemeColor } from "../common"; import Icon from "../Icon"; import "./Menu.css"; @@ -12,7 +12,7 @@ export type MenuItem = } | { type: "button"; - text: Text; + text: I18nText; icon?: string; color?: ThemeColor; onClick?: (e: MouseEvent) => void; diff --git a/FrontEnd/src/components/tab/TabBar.tsx b/FrontEnd/src/components/tab/TabBar.tsx index 601f664d..6957b700 100644 --- a/FrontEnd/src/components/tab/TabBar.tsx +++ b/FrontEnd/src/components/tab/TabBar.tsx @@ -2,13 +2,13 @@ import { ReactNode } from "react"; import { Link } from "react-router-dom"; import classNames from "classnames"; -import { Text, ThemeColor, useC } from "../common"; +import { I18nText, ThemeColor, useC } from "../common"; import "./TabBar.css"; export interface Tab { name: string; - text: Text; + text: I18nText; link?: string; onClick?: () => void; } diff --git a/FrontEnd/src/components/tab/TabPages.tsx b/FrontEnd/src/components/tab/TabPages.tsx index ab45ffdf..71065b01 100644 --- a/FrontEnd/src/components/tab/TabPages.tsx +++ b/FrontEnd/src/components/tab/TabPages.tsx @@ -1,7 +1,7 @@ import { ReactNode, useState } from "react"; import classNames from "classnames"; -import { Text, UiLogicError } from "../common"; +import { I18nText, UiLogicError } from "../common"; import Tabs from "./TabBar"; @@ -9,7 +9,7 @@ import "./TabPages.css"; interface TabPage { name: string; - text: Text; + text: I18nText; page: ReactNode; } diff --git a/FrontEnd/src/i18n.ts b/FrontEnd/src/i18n.ts deleted file mode 100644 index 3166ec3c..00000000 --- a/FrontEnd/src/i18n.ts +++ /dev/null @@ -1,114 +0,0 @@ -import i18n, { BackendModule } from "i18next"; -import LanguageDetector from "i18next-browser-languagedetector"; -import { initReactI18next } from "react-i18next"; - -const backend: BackendModule = { - type: "backend", - init() { - /* do nothing */ - }, - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async read(language, namespace) { - if (namespace === "translation") { - if (language === "en") { - return await import("./locales/en/translation.json"); - } else if (language === "zh") { - return await import("./locales/zh/translation.json"); - } else { - throw Error(`Language ${language} is not supported.`); - } - } else if (namespace === "admin") { - if (language === "en") { - return await import("./locales/en/admin.json"); - } else if (language === "zh") { - return await import("./locales/zh/admin.json"); - } else { - throw Error(`Language ${language} is not supported.`); - } - } else { - throw Error(`Namespace ${namespace} is not supported.`); - } - }, -}; - -export const i18nPromise = i18n - .use(LanguageDetector) - .use(backend) - .use(initReactI18next) // bind react-i18next to the instance - .init({ - fallbackLng: false, - lowerCaseLng: true, - - debug: process.env.NODE_ENV === "development", - - interpolation: { - escapeValue: false, // not needed for react!! - }, - - // react i18next special options (optional) - // override if needed - omit if ok with defaults - /* - react: { - bindI18n: 'languageChanged', - bindI18nStore: '', - transEmptyNodeValue: '', - transSupportBasicHtmlNodes: true, - transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'], - useSuspense: true, - } - */ - }); - -if (module.hot) { - module.hot.accept( - [ - "./locales/en/translation.json", - "./locales/zh/translation.json", - "./locales/en/admin.json", - "./locales/zh/admin.json", - ], - () => { - void i18n.reloadResources(); - }, - ); -} - -export default i18n; - -export type I18nText = - | string - | { type: "text" | "custom"; value: string } - | { type: "i18n"; value: string }; - -type T = typeof i18n.t; - -export function convertI18nText(text: I18nText, t: T): string; -export function convertI18nText( - text: I18nText | null | undefined, - t: T, -): string | null; -export function convertI18nText( - text: I18nText | null | undefined, - t: T, -): string | null { - if (text == null) { - return null; - } else if (typeof text === "string") { - return t(text); - } else if (text.type === "i18n") { - return t(text.value); - } else { - return text.value; - } -} - -export interface C { - (text: I18nText): string; - (text: I18nText | null | undefined): string | null; -} - -export function createC(t: T): C { - return ((text) => convertI18nText(text, t)) as C; -} - -export const c = createC(i18n.t); diff --git a/FrontEnd/src/i18n/backend.ts b/FrontEnd/src/i18n/backend.ts new file mode 100644 index 00000000..92f0c12f --- /dev/null +++ b/FrontEnd/src/i18n/backend.ts @@ -0,0 +1,33 @@ +import { BackendModule } from "i18next"; + + const backend: BackendModule = { + type: "backend", + init() { + /* do nothing */ + }, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async read(language, namespace) { + if (namespace === "translation") { + if (language === "en") { + return await import("./translations/en/index.json"); + } else if (language === "zh") { + return await import("./translations/zh/index.json"); + } else { + throw Error(`Language ${language} is not supported.`); + } + } else if (namespace === "admin") { + if (language === "en") { + return await import("./translations/en/admin.json"); + } else if (language === "zh") { + return await import("./translations/zh/admin.json"); + } else { + throw Error(`Language ${language} is not supported.`); + } + } else { + throw Error(`Namespace ${namespace} is not supported.`); + } + }, +}; + +export default backend; + diff --git a/FrontEnd/src/i18n/index.ts b/FrontEnd/src/i18n/index.ts new file mode 100644 index 00000000..4bd6dc28 --- /dev/null +++ b/FrontEnd/src/i18n/index.ts @@ -0,0 +1,3 @@ +import "./setup"; +export { default as i18n } from "i18next"; +export * from "./text"; diff --git a/FrontEnd/src/i18n/setup.ts b/FrontEnd/src/i18n/setup.ts new file mode 100644 index 00000000..63dd40ed --- /dev/null +++ b/FrontEnd/src/i18n/setup.ts @@ -0,0 +1,50 @@ +import i18n from "i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { initReactI18next } from "react-i18next"; + +import backend from "./backend"; + +void i18n + .use(LanguageDetector) + .use(backend) + .use(initReactI18next) // bind react-i18next to the instance + .init({ + fallbackLng: false, + lowerCaseLng: true, + + debug: process.env.NODE_ENV === "development", + + interpolation: { + escapeValue: false, // not needed for react!! + }, + + // react i18next special options (optional) + // override if needed - omit if ok with defaults + /* + react: { + bindI18n: 'languageChanged', + bindI18nStore: '', + transEmptyNodeValue: '', + transSupportBasicHtmlNodes: true, + transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'], + useSuspense: true, + } + */ + }); + +if (module.hot) { + module.hot.accept( + [ + "./translations/en/index.json", + "./translations/zh/index.json", + "./translations/en/admin.json", + "./translations/zh/admin.json", + ], + () => { + void i18n.reloadResources(); + }, + ); +} + +export default i18n; + diff --git a/FrontEnd/src/i18n/text.ts b/FrontEnd/src/i18n/text.ts new file mode 100644 index 00000000..f8f7e7e6 --- /dev/null +++ b/FrontEnd/src/i18n/text.ts @@ -0,0 +1,35 @@ +import i18n from "i18next"; +import { useTranslation } from "react-i18next"; + +export type I18nText = + | string + | { type: "text" | "custom"; value: string } + | { type: "i18n"; value: string }; + +type T = typeof i18n.t; + +export function convertI18nText(text: I18nText, t: T): string { + if (typeof text === "string") { + return t(text); + } else if (text.type === "i18n") { + return t(text.value); + } else { + return text.value; + } +} + +export interface C { + (text: I18nText): string; +} + +export function createC(t: T): C { + return ((text) => convertI18nText(text, t)) as C; +} + +export const c = createC(i18n.t); + +export function useC(ns?: string): C { + const { t } = useTranslation(ns); + return createC(t); +} + diff --git a/FrontEnd/src/i18n/translations/en/admin.json b/FrontEnd/src/i18n/translations/en/admin.json new file mode 100644 index 00000000..ddb3ffad --- /dev/null +++ b/FrontEnd/src/i18n/translations/en/admin.json @@ -0,0 +1,35 @@ +{ + "nav": { + "users": "Users", + "more": "More" + }, + "create": "Create", + "user": { + "username": "Username: ", + "password": "Password: ", + "nickname": "Nickname: ", + "uniqueId": "Unique ID: ", + "permissions": "Permissions: ", + "modify": "Modify", + "modifyPermissions": "Modify Permissions", + "delete": "Delete", + "dialog": { + "create": { + "title": "Create User", + "prompt": "You are creating a new user." + }, + "delete": { + "title": "Delete user", + "prompt": "You are deleting <1>username . Caution: This can't be undo." + }, + "modify": { + "title": "Modify User", + "prompt": "You are modifying user <1>username ." + }, + "modifyPermissions": { + "title": "Modify User Permissions", + "prompt": "You are modifying permissions of user <1>username ." + } + } + } +} diff --git a/FrontEnd/src/i18n/translations/en/index.json b/FrontEnd/src/i18n/translations/en/index.json new file mode 100644 index 00000000..1b43357c --- /dev/null +++ b/FrontEnd/src/i18n/translations/en/index.json @@ -0,0 +1,228 @@ +{ + "welcome": "Welcome!", + "search": "Search", + "edit": "Edit", + "image": "Image", + "done": "Done", + "preview": "Preview", + "delete": "Delete", + "changeProperty": "Change Property", + "loadFailReload": "Load failed, <1>click here to reload.", + "error": { + "network": "Network error.", + "unknown": "Unknown error." + }, + "connectionState": { + "Connected": "Connected", + "Connecting": "Connecting", + "Disconnected": "Disconnected", + "Disconnecting": "Disconnecting", + "Reconnecting": "Reconnecting" + }, + "visibility": { + "private": "Private For Me", + "register": "Only Registered Users", + "public": "Public To Everyone" + }, + "register": { + "register": "Register", + "username": "Username", + "password": "Password", + "confirmPassword": "Confirm Password", + "registerCode": "Register Code", + "error": { + "usernameEmpty": "Username can't be empty.", + "passwordEmpty": "Password can't be emtpy.", + "confirmPasswordWrong": "Password does not match.", + "registerCodeEmpty": "Register code can't be empty.", + "registerCodeInvalid": "Register code is invalid." + } + }, + "nav": { + "settings": "Settings", + "login": "Login", + "about": "About", + "administration": "Administration" + }, + "chooseImage": "Choose a image", + "loadImageError": "Failed to load image.", + "home": { + "loadingHighlightTimelines": "Loading highlight timelines...", + "loadedHighlightTimelines": "Here are some highlight timelines💡", + "errorHighlightTimelines": "Failed to load highlight timelines, please try reloading!", + "bookmarkTimeline": "Bookmark Timelines", + "highlightTimeline": "Highlight Timelines", + "relatedTimeline": "Timelines You Participate", + "message": { + "moveHighlightFail": "Failed to move highlight timeline.", + "deleteHighlightFail": "Failed to delete highlight timeline.", + "moveBookmarkFail": "Failed to move bookmark timeline.", + "deleteBookmarkFail": "Failed to delete bookmark timeline." + }, + "createButton": "Create Timeline", + "createDialog": { + "title": "Create Timeline!", + "name": "Name", + "nameFormat": "Name must consist of only letter including non-English letter, digit, hyphen(-) and underline(_) and be no longer than 26.", + "badFormat": "Bad format.", + "noEmpty": "Empty is not allowed.", + "tooLong": "Too long." + } + }, + "operationDialog": { + "retry": "Retry", + "nextStep": "Next", + "previousStep": "Previous", + "confirm": "Confirm", + "cancel": "Cancel", + "ok": "OK!", + "processing": "Processing...", + "success": "Success!", + "error": "An error occurred." + }, + "timeline": { + "messageCantSee": "Sorry, you are not allowed to see this timeline.😅", + "userNotExist": "The user does not exist!", + "timelineNotExist": "The timeline does not exist!", + "manage": "Manage", + "memberButton": "Member", + "send": "Send", + "deletePostFailed": "Failed to delete post.", + "sendPostFailed": "Failed to send post.", + "dropDraft": "Drop Draft", + "confirmLeave": "Are you sure to leave? All content you typed would be lost.", + "postNoLogin": "Please login to send post.", + "visibility": { + "public": "public to everyone", + "register": "only registed people can see", + "private": "only members can see" + }, + "visibilityTooltip": { + "public": "Everyone including those without accounts can see content of the timeline.", + "register": "Only those who have an account and logined can see content of the timeline.", + "private": "Only members of this timeline can see content of the timeline." + }, + "dialogChangeProperty": { + "title": "Change Timeline Properties", + "titleField": "Title", + "visibility": "Visibility", + "description": "Description", + "color": "Color" + }, + "changePostPropertyDialog": { + "title": "Change Post Properties", + "time": "Date and time", + "timeEmpty": "You must select a time." + }, + "member": { + "noUserAvailableToAdd": "Sorry, no user available to be a member in search result.", + "add": "Add", + "remove": "Remove" + }, + "manageItem": { + "nickname": "Nickname", + "avatar": "Avatar", + "property": "Timeline Property", + "member": "Timeline Member", + "delete": "Delete Timeline" + }, + "deleteDialog": { + "title": "Delete Timeline", + "inputPrompt": "This is a dangerous action. If you are sure to delete timeline<1>{{name}}, please input its name below and click confirm button.", + "notMatch": "Name does not match." + }, + "post": { + "type": { + "text": "Plain Text", + "markdown": "Markdown", + "image": "Image" + }, + "deleteDialog": { + "title": "Confirm Delete", + "prompt": "Are you sure to delete the post? This operation is not recoverable." + } + }, + "addHighlightFail": "Failed to add highlight.", + "removeHighlightFail": "Failed to remove highlight.", + "addBookmarkFail": "Failed to add bookmark.", + "removeBookmarkFail": "Failed to remove bookmark." + }, + "searchPage": { + "loading": "Loading search result...", + "input": "Input something and search!", + "noResult": "Sorry, there is no satisfied results." + }, + "user": { + "username": "username", + "password": "password", + "login": "login", + "rememberMe": "Remember Me", + "welcomeBack": "Welcome back!", + "tokenInvalid": "Your authentication token is not valid any more. Please log in again!" + }, + "login": { + "emptyUsername": "Username can't be empty.", + "emptyPassword": "Password can't be empty.", + "badCredential": "Username or password is invalid.", + "alreadyLogin": "Already login! Redirect to home page in 3s!", + "noAccount": "If you don't have an account and know a register code, then click <1>here to register." + }, + "settings": { + "subheader": { + "account": "Account", + "customization": "Customization" + }, + "languagePrimary": "Choose display language", + "languageSecondary": "You language preference will be saved locally. Next time you visit this page, last language option will be used.", + "changePassword": "Change account's password", + "logout": "Log out this account", + "changeAvatar": "Change avatar", + "changeNickname": "Change nickname", + "myRegisterCode": "My register code:", + "myRegisterCodeDesc": "Click to create a new register code.", + "renewRegisterCode": "Renew Register Code", + "renewRegisterCodeDesc": "Confirm to renew register code? The old one will no longer be used.", + "myRegisterCodeCopied": "Register code copied!", + "dialogChangePassword": { + "title": "Change Password", + "prompt": "You are changing your password. You need to input the correct old password. After change, you need to login again and all old login will be invalid.", + "inputOldPassword": "Old password", + "inputNewPassword": "New password", + "inputRetypeNewPassword": "Retype new password", + "errorEmptyOldPassword": "Old password can't be empty.", + "errorEmptyNewPassword": "New password can't be empty.", + "errorRetypeNotMatch": "Password retyped does not match." + }, + "dialogConfirmLogout": { + "title": "Confirm Logout", + "prompt": "Are you sure to log out? All cached data in the browser will be deleted." + }, + "dialogChangeNickname": { + "title": "Change Nickname", + "inputLabel": "New nickname" + }, + "dialogChangeAvatar": { + "title": "Change Avatar", + "previewImgAlt": "preview", + "prompt": { + "select": "Please select a picture.", + "crop": "Please crop the picture.", + "processingCrop": "Cropping picture...", + "uploading": "Uploading...", + "preview": "Please preview avatar" + }, + "upload": "upload" + } + }, + "about": { + "credits": { + "title": "Credits", + "content": "Timeline stands on shoulders of giants. Special appreciation for many open source projects listed below or not. Related licenses could be found in GitHub repository.", + "frontend": "Frontend", + "backend": "Backend" + } + }, + "admin": { + "title": "admin" + } +} diff --git a/FrontEnd/src/i18n/translations/zh/admin.json b/FrontEnd/src/i18n/translations/zh/admin.json new file mode 100644 index 00000000..edd1cabd --- /dev/null +++ b/FrontEnd/src/i18n/translations/zh/admin.json @@ -0,0 +1,35 @@ +{ + "nav": { + "users": "用户", + "more": "更多" + }, + "create": "创建", + "user": { + "username": "用户名:", + "password": "密码:", + "nickname": "昵称:", + "uniqueId": "唯一ID:", + "permissions": "权限:", + "modify": "修改", + "modifyPermissions": "修改权限", + "delete": "删除", + "dialog": { + "create": { + "title": "创建用户", + "prompt": "您正在创建一个新用户。" + }, + "delete": { + "title": "删除用户", + "prompt": "您正在删除用户 <1>username 。注意:此操作不可撤销。" + }, + "modify": { + "title": "修改用户", + "prompt": "您正在修改用户 <1>username 。" + }, + "modifyPermissions": { + "title": "修改用户权限", + "prompt": "您正在修改用户 <1>username 的权限。" + } + } + } +} diff --git a/FrontEnd/src/i18n/translations/zh/index.json b/FrontEnd/src/i18n/translations/zh/index.json new file mode 100644 index 00000000..dc0d6672 --- /dev/null +++ b/FrontEnd/src/i18n/translations/zh/index.json @@ -0,0 +1,241 @@ +{ + "welcome": "欢迎!", + "search": "搜索", + "edit": "编辑", + "image": "图片", + "done": "完成", + "preview": "预览", + "loadFailReload": "加载失败,<1>点击重试。", + "delete": "删除", + "changeProperty": "修改属性", + "error": { + "network": "网络错误。", + "unknown": "未知错误。" + }, + "visibility": { + "private": "仅自己可见", + "register": "仅注册用户可见", + "public": "对所有人公开" + }, + "register": { + "register": "注册", + "username": "用户名", + "password": "密码", + "confirmPassword": "确认密码", + "registerCode": "注册码", + "error": { + "usernameEmpty": "用户名不能为空。", + "passwordEmpty": "密码不能为空。", + "confirmPasswordWrong": "密码不匹配。", + "registerCodeEmpty": "注册码不能为空。", + "registerCodeInvalid": "注册码无效。" + } + }, + "connectionState": { + "Connected": "已连接", + "Connecting": "正在连接", + "Disconnected": "已断开连接", + "Disconnecting": "正在断开连接", + "Reconnecting": "正在重新连接" + }, + "nav": { + "settings": "设置", + "login": "登陆", + "about": "关于", + "administration": "管理" + }, + "chooseImage": "选择一个图片", + "loadImageError": "加载图片失败", + "home": { + "loadingHighlightTimelines": "正在加载高光时间线...", + "loadedHighlightTimelines": "康康以下这些高光时间线💡", + "errorHighlightTimelines": "加载高光时间线失败,刷新试试!", + "bookmarkTimeline": "书签时间线", + "highlightTimeline": "高光时间线", + "relatedTimeline": "参与的时间线", + "message": { + "moveHighlightFail": "移动高光时间线失败。", + "deleteHighlightFail": "删除高光时间线失败。", + "moveBookmarkFail": "移动书签时间线失败。", + "deleteBookmarkFail": "删除书签时间线失败。" + }, + "createButton": "创建时间线", + "createDialog": { + "title": "创建时间线!", + "name": "名字", + "nameFormat": "名字只能由字母、汉字、数字、下划线(_)和连字符(-)构成,且长度不能超过26.", + "badFormat": "格式错误", + "noEmpty": "不能为空", + "tooLong": "太长了" + } + }, + "operationDialog": { + "retry": "重试", + "nextStep": "下一步", + "previousStep": "上一步", + "confirm": "确定", + "cancel": "取消", + "ok": "好的!", + "processing": "处理中...", + "success": "成功!", + "error": "出错啦!" + }, + "timeline": { + "messageCantSee": "不好意思,你没有权限查看这个时间线。😅", + "userNotExist": "该用户不存在!", + "timelineNotExist": "该时间线不存在!", + "manage": "管理", + "memberButton": "成员", + "send": "发送", + "deletePostFailed": "删除消息失败。", + "sendPostFailed": "发送消息失败。", + "dropDraft": "放弃草稿", + "confirmLeave": "确定要离开吗?所有输入的内容将会丢失。", + "postNoLogin": "登陆后即可发表消息!", + "visibility": { + "public": "对所有人公开", + "register": "仅注册可见", + "private": "仅成员可见" + }, + "visibilityTooltip": { + "public": "所有人都可以看到这个时间线的内容,包括没有注册的人。", + "register": "只有拥有本网站的账号且登陆了的人才能看到这个时间线的内容。", + "private": "只有这个时间线的成员可以看到这个时间线的内容。" + }, + "dialogChangeProperty": { + "title": "修改时间线属性", + "titleField": "标题", + "visibility": "可见性", + "description": "描述", + "color": "颜色" + }, + "changePostPropertyDialog": { + "title": "修改消息属性", + "time": "时间", + "timeEmpty": "你必须选择一个时间。" + }, + "member": { + "noUserAvailableToAdd": "搜索结果显示没有可以添加为成员的用户。", + "add": "添加", + "remove": "移除" + }, + "manageItem": { + "nickname": "昵称", + "avatar": "头像", + "property": "时间线属性", + "member": "时间线成员", + "delete": "删除时间线" + }, + "deleteDialog": { + "title": "删除时间线", + "inputPrompt": "这是一个危险的操作。如果您确认要删除时间线<1>{{name}},请在下面输入它的名字并点击确认。", + "notMatch": "名字不匹配" + }, + "post": { + "type": { + "text": "纯文本", + "markdown": "Markdown", + "image": "图片" + }, + "deleteDialog": { + "title": "确认删除", + "prompt": "确定删除这个消息?这个操作不可撤销。" + } + }, + "addHighlightFail": "添加高光失败。", + "removeHighlightFail": "删除高光失败。", + "addBookmarkFail": "添加书签失败。", + "removeBookmarkFail": "删除书签失败。" + }, + "searchPage": { + "loading": "加载搜索结果中...", + "input": "输入一些东西来搜索!", + "noResult": "对不起,没有符合条件的结果。" + }, + "user": { + "username": "用户名", + "password": "密码", + "login": "登录", + "rememberMe": "记住我", + "welcomeBack": "欢迎回来!", + "tokenInvalid": "您的登录信息已失效,请重新登陆!" + }, + "login": { + "emptyUsername": "用户名不能为空。", + "emptyPassword": "密码不能为空。", + "badCredential": "用户名或密码错误。", + "alreadyLogin": "已经登陆,三秒后导航到首页!", + "noAccount": "如果你没有账号但有一个注册码,请点击<1>这里注册账号。" + }, + "settings": { + "subheader": { + "account": "账户", + "customization": "个性化" + }, + "languagePrimary": "选择显示的语言。", + "languageSecondary": "您的语言偏好将会存储在本地,下次浏览时将自动使用上次保存的语言选项。", + "changePassword": "更改账号的密码", + "logout": "注销此账号", + "changeAvatar": "更改头像", + "changeNickname": "更改昵称", + "changeBookmarkVisibility": "修改书签时间线可见性", + "myRegisterCode": "我的注册码:", + "myRegisterCodeDesc": "点击以创建新的注册码。", + "renewRegisterCode": "创建新的注册码", + "renewRegisterCodeDesc": "确定要创建新的注册码吗?旧的注册码将无法再使用。", + "myRegisterCodeCopied": "注册码已复制!", + "dialogChangePassword": { + "title": "修改密码", + "prompt": "您正在修改密码,您需要输入正确的旧密码。成功修改后您需要重新登陆,而且以前所有的登录都会失效。", + "inputOldPassword": "旧密码", + "inputNewPassword": "新密码", + "inputRetypeNewPassword": "再次输入新密码", + "errorEmptyOldPassword": "旧密码不能为空。", + "errorEmptyNewPassword": "新密码不能为空", + "errorRetypeNotMatch": "两次输入的密码不一致" + }, + "dialogConfirmLogout": { + "title": "确定注销", + "prompt": "您确定注销此账号?这将删除所有已经缓存在浏览器的数据。" + }, + "dialogChangeNickname": { + "title": "更改昵称", + "inputLabel": "新昵称" + }, + "dialogChangeAvatar": { + "title": "修改头像", + "previewImgAlt": "预览", + "prompt": { + "select": "请选择一个图片", + "crop": "请裁剪图片", + "processingCrop": "正在裁剪图片", + "uploading": "正在上传", + "preview": "请预览图片" + }, + "upload": "上传" + } + }, + "about": { + "author": { + "title": "网站作者", + "name": "名字:", + "introduction": "简介:", + "introductionContent": "一个基于巧合编程的代码爱好者。", + "links": "链接:" + }, + "site": { + "title": "网站信息", + "content": "这个网站的名字叫 <1>Timeline,是一个以<3>时间线为核心概念的 Web App . 它的前端和后端都是由<5>我开发,并且在 GitHub 上开源。大家可以相对轻松的把它们部署在自己的服务器上,这也是我的目标之一。欢迎大家前往 GitHub 仓库提出任何意见。", + "repo": "GitHub 仓库" + }, + "credits": { + "title": "鸣谢", + "content": "Timeline 是站在巨人肩膀上的作品,感谢以下列出的和其他未列出的许多开源项目,相关 License 请在 GitHub 仓库中查看。", + "frontend": "前端:", + "backend": "后端:" + } + }, + "admin": { + "title": "管理" + } +} diff --git a/FrontEnd/src/locales/en/admin.json b/FrontEnd/src/locales/en/admin.json deleted file mode 100644 index ddb3ffad..00000000 --- a/FrontEnd/src/locales/en/admin.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "nav": { - "users": "Users", - "more": "More" - }, - "create": "Create", - "user": { - "username": "Username: ", - "password": "Password: ", - "nickname": "Nickname: ", - "uniqueId": "Unique ID: ", - "permissions": "Permissions: ", - "modify": "Modify", - "modifyPermissions": "Modify Permissions", - "delete": "Delete", - "dialog": { - "create": { - "title": "Create User", - "prompt": "You are creating a new user." - }, - "delete": { - "title": "Delete user", - "prompt": "You are deleting <1>username . Caution: This can't be undo." - }, - "modify": { - "title": "Modify User", - "prompt": "You are modifying user <1>username ." - }, - "modifyPermissions": { - "title": "Modify User Permissions", - "prompt": "You are modifying permissions of user <1>username ." - } - } - } -} diff --git a/FrontEnd/src/locales/en/translation.json b/FrontEnd/src/locales/en/translation.json deleted file mode 100644 index 1b43357c..00000000 --- a/FrontEnd/src/locales/en/translation.json +++ /dev/null @@ -1,228 +0,0 @@ -{ - "welcome": "Welcome!", - "search": "Search", - "edit": "Edit", - "image": "Image", - "done": "Done", - "preview": "Preview", - "delete": "Delete", - "changeProperty": "Change Property", - "loadFailReload": "Load failed, <1>click here to reload.", - "error": { - "network": "Network error.", - "unknown": "Unknown error." - }, - "connectionState": { - "Connected": "Connected", - "Connecting": "Connecting", - "Disconnected": "Disconnected", - "Disconnecting": "Disconnecting", - "Reconnecting": "Reconnecting" - }, - "visibility": { - "private": "Private For Me", - "register": "Only Registered Users", - "public": "Public To Everyone" - }, - "register": { - "register": "Register", - "username": "Username", - "password": "Password", - "confirmPassword": "Confirm Password", - "registerCode": "Register Code", - "error": { - "usernameEmpty": "Username can't be empty.", - "passwordEmpty": "Password can't be emtpy.", - "confirmPasswordWrong": "Password does not match.", - "registerCodeEmpty": "Register code can't be empty.", - "registerCodeInvalid": "Register code is invalid." - } - }, - "nav": { - "settings": "Settings", - "login": "Login", - "about": "About", - "administration": "Administration" - }, - "chooseImage": "Choose a image", - "loadImageError": "Failed to load image.", - "home": { - "loadingHighlightTimelines": "Loading highlight timelines...", - "loadedHighlightTimelines": "Here are some highlight timelines💡", - "errorHighlightTimelines": "Failed to load highlight timelines, please try reloading!", - "bookmarkTimeline": "Bookmark Timelines", - "highlightTimeline": "Highlight Timelines", - "relatedTimeline": "Timelines You Participate", - "message": { - "moveHighlightFail": "Failed to move highlight timeline.", - "deleteHighlightFail": "Failed to delete highlight timeline.", - "moveBookmarkFail": "Failed to move bookmark timeline.", - "deleteBookmarkFail": "Failed to delete bookmark timeline." - }, - "createButton": "Create Timeline", - "createDialog": { - "title": "Create Timeline!", - "name": "Name", - "nameFormat": "Name must consist of only letter including non-English letter, digit, hyphen(-) and underline(_) and be no longer than 26.", - "badFormat": "Bad format.", - "noEmpty": "Empty is not allowed.", - "tooLong": "Too long." - } - }, - "operationDialog": { - "retry": "Retry", - "nextStep": "Next", - "previousStep": "Previous", - "confirm": "Confirm", - "cancel": "Cancel", - "ok": "OK!", - "processing": "Processing...", - "success": "Success!", - "error": "An error occurred." - }, - "timeline": { - "messageCantSee": "Sorry, you are not allowed to see this timeline.😅", - "userNotExist": "The user does not exist!", - "timelineNotExist": "The timeline does not exist!", - "manage": "Manage", - "memberButton": "Member", - "send": "Send", - "deletePostFailed": "Failed to delete post.", - "sendPostFailed": "Failed to send post.", - "dropDraft": "Drop Draft", - "confirmLeave": "Are you sure to leave? All content you typed would be lost.", - "postNoLogin": "Please login to send post.", - "visibility": { - "public": "public to everyone", - "register": "only registed people can see", - "private": "only members can see" - }, - "visibilityTooltip": { - "public": "Everyone including those without accounts can see content of the timeline.", - "register": "Only those who have an account and logined can see content of the timeline.", - "private": "Only members of this timeline can see content of the timeline." - }, - "dialogChangeProperty": { - "title": "Change Timeline Properties", - "titleField": "Title", - "visibility": "Visibility", - "description": "Description", - "color": "Color" - }, - "changePostPropertyDialog": { - "title": "Change Post Properties", - "time": "Date and time", - "timeEmpty": "You must select a time." - }, - "member": { - "noUserAvailableToAdd": "Sorry, no user available to be a member in search result.", - "add": "Add", - "remove": "Remove" - }, - "manageItem": { - "nickname": "Nickname", - "avatar": "Avatar", - "property": "Timeline Property", - "member": "Timeline Member", - "delete": "Delete Timeline" - }, - "deleteDialog": { - "title": "Delete Timeline", - "inputPrompt": "This is a dangerous action. If you are sure to delete timeline<1>{{name}}, please input its name below and click confirm button.", - "notMatch": "Name does not match." - }, - "post": { - "type": { - "text": "Plain Text", - "markdown": "Markdown", - "image": "Image" - }, - "deleteDialog": { - "title": "Confirm Delete", - "prompt": "Are you sure to delete the post? This operation is not recoverable." - } - }, - "addHighlightFail": "Failed to add highlight.", - "removeHighlightFail": "Failed to remove highlight.", - "addBookmarkFail": "Failed to add bookmark.", - "removeBookmarkFail": "Failed to remove bookmark." - }, - "searchPage": { - "loading": "Loading search result...", - "input": "Input something and search!", - "noResult": "Sorry, there is no satisfied results." - }, - "user": { - "username": "username", - "password": "password", - "login": "login", - "rememberMe": "Remember Me", - "welcomeBack": "Welcome back!", - "tokenInvalid": "Your authentication token is not valid any more. Please log in again!" - }, - "login": { - "emptyUsername": "Username can't be empty.", - "emptyPassword": "Password can't be empty.", - "badCredential": "Username or password is invalid.", - "alreadyLogin": "Already login! Redirect to home page in 3s!", - "noAccount": "If you don't have an account and know a register code, then click <1>here to register." - }, - "settings": { - "subheader": { - "account": "Account", - "customization": "Customization" - }, - "languagePrimary": "Choose display language", - "languageSecondary": "You language preference will be saved locally. Next time you visit this page, last language option will be used.", - "changePassword": "Change account's password", - "logout": "Log out this account", - "changeAvatar": "Change avatar", - "changeNickname": "Change nickname", - "myRegisterCode": "My register code:", - "myRegisterCodeDesc": "Click to create a new register code.", - "renewRegisterCode": "Renew Register Code", - "renewRegisterCodeDesc": "Confirm to renew register code? The old one will no longer be used.", - "myRegisterCodeCopied": "Register code copied!", - "dialogChangePassword": { - "title": "Change Password", - "prompt": "You are changing your password. You need to input the correct old password. After change, you need to login again and all old login will be invalid.", - "inputOldPassword": "Old password", - "inputNewPassword": "New password", - "inputRetypeNewPassword": "Retype new password", - "errorEmptyOldPassword": "Old password can't be empty.", - "errorEmptyNewPassword": "New password can't be empty.", - "errorRetypeNotMatch": "Password retyped does not match." - }, - "dialogConfirmLogout": { - "title": "Confirm Logout", - "prompt": "Are you sure to log out? All cached data in the browser will be deleted." - }, - "dialogChangeNickname": { - "title": "Change Nickname", - "inputLabel": "New nickname" - }, - "dialogChangeAvatar": { - "title": "Change Avatar", - "previewImgAlt": "preview", - "prompt": { - "select": "Please select a picture.", - "crop": "Please crop the picture.", - "processingCrop": "Cropping picture...", - "uploading": "Uploading...", - "preview": "Please preview avatar" - }, - "upload": "upload" - } - }, - "about": { - "credits": { - "title": "Credits", - "content": "Timeline stands on shoulders of giants. Special appreciation for many open source projects listed below or not. Related licenses could be found in GitHub repository.", - "frontend": "Frontend", - "backend": "Backend" - } - }, - "admin": { - "title": "admin" - } -} diff --git a/FrontEnd/src/locales/zh/admin.json b/FrontEnd/src/locales/zh/admin.json deleted file mode 100644 index edd1cabd..00000000 --- a/FrontEnd/src/locales/zh/admin.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "nav": { - "users": "用户", - "more": "更多" - }, - "create": "创建", - "user": { - "username": "用户名:", - "password": "密码:", - "nickname": "昵称:", - "uniqueId": "唯一ID:", - "permissions": "权限:", - "modify": "修改", - "modifyPermissions": "修改权限", - "delete": "删除", - "dialog": { - "create": { - "title": "创建用户", - "prompt": "您正在创建一个新用户。" - }, - "delete": { - "title": "删除用户", - "prompt": "您正在删除用户 <1>username 。注意:此操作不可撤销。" - }, - "modify": { - "title": "修改用户", - "prompt": "您正在修改用户 <1>username 。" - }, - "modifyPermissions": { - "title": "修改用户权限", - "prompt": "您正在修改用户 <1>username 的权限。" - } - } - } -} diff --git a/FrontEnd/src/locales/zh/translation.json b/FrontEnd/src/locales/zh/translation.json deleted file mode 100644 index dc0d6672..00000000 --- a/FrontEnd/src/locales/zh/translation.json +++ /dev/null @@ -1,241 +0,0 @@ -{ - "welcome": "欢迎!", - "search": "搜索", - "edit": "编辑", - "image": "图片", - "done": "完成", - "preview": "预览", - "loadFailReload": "加载失败,<1>点击重试。", - "delete": "删除", - "changeProperty": "修改属性", - "error": { - "network": "网络错误。", - "unknown": "未知错误。" - }, - "visibility": { - "private": "仅自己可见", - "register": "仅注册用户可见", - "public": "对所有人公开" - }, - "register": { - "register": "注册", - "username": "用户名", - "password": "密码", - "confirmPassword": "确认密码", - "registerCode": "注册码", - "error": { - "usernameEmpty": "用户名不能为空。", - "passwordEmpty": "密码不能为空。", - "confirmPasswordWrong": "密码不匹配。", - "registerCodeEmpty": "注册码不能为空。", - "registerCodeInvalid": "注册码无效。" - } - }, - "connectionState": { - "Connected": "已连接", - "Connecting": "正在连接", - "Disconnected": "已断开连接", - "Disconnecting": "正在断开连接", - "Reconnecting": "正在重新连接" - }, - "nav": { - "settings": "设置", - "login": "登陆", - "about": "关于", - "administration": "管理" - }, - "chooseImage": "选择一个图片", - "loadImageError": "加载图片失败", - "home": { - "loadingHighlightTimelines": "正在加载高光时间线...", - "loadedHighlightTimelines": "康康以下这些高光时间线💡", - "errorHighlightTimelines": "加载高光时间线失败,刷新试试!", - "bookmarkTimeline": "书签时间线", - "highlightTimeline": "高光时间线", - "relatedTimeline": "参与的时间线", - "message": { - "moveHighlightFail": "移动高光时间线失败。", - "deleteHighlightFail": "删除高光时间线失败。", - "moveBookmarkFail": "移动书签时间线失败。", - "deleteBookmarkFail": "删除书签时间线失败。" - }, - "createButton": "创建时间线", - "createDialog": { - "title": "创建时间线!", - "name": "名字", - "nameFormat": "名字只能由字母、汉字、数字、下划线(_)和连字符(-)构成,且长度不能超过26.", - "badFormat": "格式错误", - "noEmpty": "不能为空", - "tooLong": "太长了" - } - }, - "operationDialog": { - "retry": "重试", - "nextStep": "下一步", - "previousStep": "上一步", - "confirm": "确定", - "cancel": "取消", - "ok": "好的!", - "processing": "处理中...", - "success": "成功!", - "error": "出错啦!" - }, - "timeline": { - "messageCantSee": "不好意思,你没有权限查看这个时间线。😅", - "userNotExist": "该用户不存在!", - "timelineNotExist": "该时间线不存在!", - "manage": "管理", - "memberButton": "成员", - "send": "发送", - "deletePostFailed": "删除消息失败。", - "sendPostFailed": "发送消息失败。", - "dropDraft": "放弃草稿", - "confirmLeave": "确定要离开吗?所有输入的内容将会丢失。", - "postNoLogin": "登陆后即可发表消息!", - "visibility": { - "public": "对所有人公开", - "register": "仅注册可见", - "private": "仅成员可见" - }, - "visibilityTooltip": { - "public": "所有人都可以看到这个时间线的内容,包括没有注册的人。", - "register": "只有拥有本网站的账号且登陆了的人才能看到这个时间线的内容。", - "private": "只有这个时间线的成员可以看到这个时间线的内容。" - }, - "dialogChangeProperty": { - "title": "修改时间线属性", - "titleField": "标题", - "visibility": "可见性", - "description": "描述", - "color": "颜色" - }, - "changePostPropertyDialog": { - "title": "修改消息属性", - "time": "时间", - "timeEmpty": "你必须选择一个时间。" - }, - "member": { - "noUserAvailableToAdd": "搜索结果显示没有可以添加为成员的用户。", - "add": "添加", - "remove": "移除" - }, - "manageItem": { - "nickname": "昵称", - "avatar": "头像", - "property": "时间线属性", - "member": "时间线成员", - "delete": "删除时间线" - }, - "deleteDialog": { - "title": "删除时间线", - "inputPrompt": "这是一个危险的操作。如果您确认要删除时间线<1>{{name}},请在下面输入它的名字并点击确认。", - "notMatch": "名字不匹配" - }, - "post": { - "type": { - "text": "纯文本", - "markdown": "Markdown", - "image": "图片" - }, - "deleteDialog": { - "title": "确认删除", - "prompt": "确定删除这个消息?这个操作不可撤销。" - } - }, - "addHighlightFail": "添加高光失败。", - "removeHighlightFail": "删除高光失败。", - "addBookmarkFail": "添加书签失败。", - "removeBookmarkFail": "删除书签失败。" - }, - "searchPage": { - "loading": "加载搜索结果中...", - "input": "输入一些东西来搜索!", - "noResult": "对不起,没有符合条件的结果。" - }, - "user": { - "username": "用户名", - "password": "密码", - "login": "登录", - "rememberMe": "记住我", - "welcomeBack": "欢迎回来!", - "tokenInvalid": "您的登录信息已失效,请重新登陆!" - }, - "login": { - "emptyUsername": "用户名不能为空。", - "emptyPassword": "密码不能为空。", - "badCredential": "用户名或密码错误。", - "alreadyLogin": "已经登陆,三秒后导航到首页!", - "noAccount": "如果你没有账号但有一个注册码,请点击<1>这里注册账号。" - }, - "settings": { - "subheader": { - "account": "账户", - "customization": "个性化" - }, - "languagePrimary": "选择显示的语言。", - "languageSecondary": "您的语言偏好将会存储在本地,下次浏览时将自动使用上次保存的语言选项。", - "changePassword": "更改账号的密码", - "logout": "注销此账号", - "changeAvatar": "更改头像", - "changeNickname": "更改昵称", - "changeBookmarkVisibility": "修改书签时间线可见性", - "myRegisterCode": "我的注册码:", - "myRegisterCodeDesc": "点击以创建新的注册码。", - "renewRegisterCode": "创建新的注册码", - "renewRegisterCodeDesc": "确定要创建新的注册码吗?旧的注册码将无法再使用。", - "myRegisterCodeCopied": "注册码已复制!", - "dialogChangePassword": { - "title": "修改密码", - "prompt": "您正在修改密码,您需要输入正确的旧密码。成功修改后您需要重新登陆,而且以前所有的登录都会失效。", - "inputOldPassword": "旧密码", - "inputNewPassword": "新密码", - "inputRetypeNewPassword": "再次输入新密码", - "errorEmptyOldPassword": "旧密码不能为空。", - "errorEmptyNewPassword": "新密码不能为空", - "errorRetypeNotMatch": "两次输入的密码不一致" - }, - "dialogConfirmLogout": { - "title": "确定注销", - "prompt": "您确定注销此账号?这将删除所有已经缓存在浏览器的数据。" - }, - "dialogChangeNickname": { - "title": "更改昵称", - "inputLabel": "新昵称" - }, - "dialogChangeAvatar": { - "title": "修改头像", - "previewImgAlt": "预览", - "prompt": { - "select": "请选择一个图片", - "crop": "请裁剪图片", - "processingCrop": "正在裁剪图片", - "uploading": "正在上传", - "preview": "请预览图片" - }, - "upload": "上传" - } - }, - "about": { - "author": { - "title": "网站作者", - "name": "名字:", - "introduction": "简介:", - "introductionContent": "一个基于巧合编程的代码爱好者。", - "links": "链接:" - }, - "site": { - "title": "网站信息", - "content": "这个网站的名字叫 <1>Timeline,是一个以<3>时间线为核心概念的 Web App . 它的前端和后端都是由<5>我开发,并且在 GitHub 上开源。大家可以相对轻松的把它们部署在自己的服务器上,这也是我的目标之一。欢迎大家前往 GitHub 仓库提出任何意见。", - "repo": "GitHub 仓库" - }, - "credits": { - "title": "鸣谢", - "content": "Timeline 是站在巨人肩膀上的作品,感谢以下列出的和其他未列出的许多开源项目,相关 License 请在 GitHub 仓库中查看。", - "frontend": "前端:", - "backend": "后端:" - } - }, - "admin": { - "title": "管理" - } -} diff --git a/FrontEnd/src/migrating/hooks/useReverseScrollPositionRemember.ts b/FrontEnd/src/migrating/hooks/useReverseScrollPositionRemember.ts new file mode 100644 index 00000000..339a12b8 --- /dev/null +++ b/FrontEnd/src/migrating/hooks/useReverseScrollPositionRemember.ts @@ -0,0 +1,58 @@ +// Not used now!!! But preserved for future use. + +import { useEffect } from "react"; + +let on = false; + +let rememberedReversePosition = getReverseScrollPosition(); + +export function getReverseScrollPosition(): number { + if (document.documentElement.scrollHeight <= window.innerHeight) { + return 0; + } else { + return ( + document.documentElement.scrollHeight - + document.documentElement.scrollTop - + window.innerHeight + ); + } +} + +export function scrollToReverseScrollPosition(reversePosition: number): void { + if (document.documentElement.scrollHeight <= window.innerHeight) return; + + const old = document.documentElement.style.scrollBehavior; + document.documentElement.style.scrollBehavior = "auto"; + + const newPosition = + document.documentElement.scrollHeight - + window.innerHeight - + reversePosition; + + window.scrollTo(0, newPosition); + + document.documentElement.style.scrollBehavior = old; +} + +const scrollListener = (): void => { + rememberedReversePosition = getReverseScrollPosition(); +}; + +const resizeObserver = new ResizeObserver(() => { + scrollToReverseScrollPosition(rememberedReversePosition); +}); + +export default function useReverseScrollPositionRemember(): void { + useEffect(() => { + if (on) return; + on = true; + window.addEventListener("scroll", scrollListener); + resizeObserver.observe(document.documentElement); + + return () => { + resizeObserver.disconnect(); + window.removeEventListener("scroll", scrollListener); + on = false; + }; + }, []); +} diff --git a/FrontEnd/src/pages/about/index.tsx b/FrontEnd/src/pages/about/index.tsx index bce64322..f95557c2 100644 --- a/FrontEnd/src/pages/about/index.tsx +++ b/FrontEnd/src/pages/about/index.tsx @@ -1,6 +1,6 @@ import "./index.css"; -import { useC } from "~src/common"; +import { useC } from "~src/components/common"; import Page from "~src/components/Page"; interface Credit { diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx index 4cdecbbb..9ede593e 100644 --- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -1,11 +1,10 @@ import { useState, ChangeEvent, ComponentPropsWithoutRef } from "react"; -import { useC, Text, UiLogicError } from "~src/common"; - import { useUser } from "~src/services/user"; import { getHttpUserClient } from "~src/http/user"; +import { useC, I18nText, UiLogicError } from "~src/components/common"; import { ImageCropper, useImageCrop } from "~src/components/ImageCropper"; import BlobImage from "~src/components/BlobImage"; import { ButtonRowV2 } from "~src/components/button"; @@ -43,7 +42,7 @@ export default function ChangeAvatarDialog({ }); const [resultBlob, setResultBlob] = useState(null); - const [message, setMessage] = useState( + const [message, setMessage] = useState( "settings.dialogChangeAvatar.prompt.select", ); diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index 3fb18e24..70df1b32 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -11,8 +11,7 @@ import classNames from "classnames"; import { useUser, userService } from "~src/services/user"; import { getHttpUserClient } from "~src/http/user"; -import { useC, Text } from "~src/common"; - +import { useC, I18nText } from "~src/components/common"; import { pushAlert } from "~src/components/alert"; import { useDialog, ConfirmDialog } from "~src/components/dialog"; import Card from "~src/components/Card"; @@ -27,7 +26,7 @@ import "./index.css"; interface SettingSectionProps extends Omit, "title"> { - title: Text; + title: I18nText; children?: ReactNode; } @@ -49,8 +48,8 @@ function SettingSection({ interface SettingItemContainerProps extends Omit, "title"> { - title: Text; - description?: Text; + title: I18nText; + description?: I18nText; danger?: boolean; extraClassName?: string; } @@ -78,7 +77,9 @@ function SettingItemContainer({ >
{c(title)}
- {c(description)} + {description && ( + {c(description)} + )}
{children}
@@ -97,7 +98,7 @@ interface SelectSettingItemProps extends Omit { options: { value: string; - label: Text; + label: I18nText; }[]; value?: string | null; onSelect: (value: string) => void; diff --git a/FrontEnd/src/pages/timeline/Timeline.tsx b/FrontEnd/src/pages/timeline/Timeline.tsx index e2ab5c71..69dfecea 100644 --- a/FrontEnd/src/pages/timeline/Timeline.tsx +++ b/FrontEnd/src/pages/timeline/Timeline.tsx @@ -115,7 +115,7 @@ export function Timeline(props: TimelineProps) { return () => { subscription.unsubscribe(); }; - }, [timelineOwner, timelineName]); + }, [timelineOwner, timelineName, reloadPosts]); useScrollToBottom(() => { console.log(`Load page ${currentPage + 1}.`); diff --git a/FrontEnd/src/pages/timeline/TimelineMember.tsx b/FrontEnd/src/pages/timeline/TimelineMember.tsx index 0812016f..4fa9cf72 100644 --- a/FrontEnd/src/pages/timeline/TimelineMember.tsx +++ b/FrontEnd/src/pages/timeline/TimelineMember.tsx @@ -1,12 +1,11 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { convertI18nText, I18nText } from "~src/common"; - import { HttpUser } from "~src/http/user"; import { getHttpSearchClient } from "~src/http/search"; import { getHttpTimelineClient, HttpTimelineInfo } from "~src/http/timeline"; +import { convertI18nText, I18nText } from "~src/components/common"; import SearchInput from "~src/components/SearchInput"; import UserAvatar from "~src/components/user/UserAvatar"; import { IconButton } from "~src/components/button"; diff --git a/FrontEnd/src/pages/timeline/edit/TimelinePostCreateView.tsx b/FrontEnd/src/pages/timeline/edit/TimelinePostCreateView.tsx index c0a80ad0..fe04bfa2 100644 --- a/FrontEnd/src/pages/timeline/edit/TimelinePostCreateView.tsx +++ b/FrontEnd/src/pages/timeline/edit/TimelinePostCreateView.tsx @@ -1,8 +1,6 @@ import { useState } from "react"; import classNames from "classnames"; -import { UiLogicError } from "~src/common"; - import { getHttpTimelineClient, HttpTimelineInfo, @@ -12,7 +10,7 @@ import { import base64 from "~src/utilities/base64"; -import { useC } from "~/src/components/common"; +import { UiLogicError, useC } from "~/src/components/common"; import { pushAlert } from "~src/components/alert"; import { IconButton, LoadingButton } from "~src/components/button"; import PopupMenu from "~src/components/menu/PopupMenu"; diff --git a/FrontEnd/src/pages/timeline/index.tsx b/FrontEnd/src/pages/timeline/index.tsx index 6cd1ded0..ee792d93 100644 --- a/FrontEnd/src/pages/timeline/index.tsx +++ b/FrontEnd/src/pages/timeline/index.tsx @@ -1,6 +1,6 @@ import { useParams } from "react-router-dom"; -import { UiLogicError } from "~src/common"; +import { UiLogicError } from "~src/components/common"; import Timeline from "./Timeline"; diff --git a/FrontEnd/src/services/alert.ts b/FrontEnd/src/services/alert.ts index 0fa37848..e968af76 100644 --- a/FrontEnd/src/services/alert.ts +++ b/FrontEnd/src/services/alert.ts @@ -1,7 +1,6 @@ import pull from "lodash/pull"; -import { I18nText } from "~src/common"; -import { ThemeColor } from "~src/components/common"; +import { I18nText, ThemeColor } from "~src/components/common"; export interface AlertInfo { type?: ThemeColor; diff --git a/FrontEnd/src/services/user.ts b/FrontEnd/src/services/user.ts index 5f682a36..0fd363f5 100644 --- a/FrontEnd/src/services/user.ts +++ b/FrontEnd/src/services/user.ts @@ -2,12 +2,12 @@ import { useState, useEffect } from "react"; import { BehaviorSubject, Observable } from "rxjs"; import { AxiosError } from "axios"; -import { UiLogicError } from "~src/common"; import { setHttpToken, axios, HttpBadRequestError } from "~src/http/common"; import { getHttpTokenClient } from "~src/http/token"; import { getHttpUserClient, HttpUser, UserPermission } from "~src/http/user"; +import { UiLogicError } from "~src/components/common"; import { pushAlert } from "~src/components/alert"; interface IAuthUser extends HttpUser { diff --git a/FrontEnd/src/utilities/hooks/use-c.ts b/FrontEnd/src/utilities/hooks/use-c.ts deleted file mode 100644 index 96195ae2..00000000 --- a/FrontEnd/src/utilities/hooks/use-c.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { C, createC } from "../../i18n"; - -export default function useC(ns?: string): C { - const { t } = useTranslation(ns); - return createC(t); -} diff --git a/FrontEnd/src/utilities/hooks/useReverseScrollPositionRemember.ts b/FrontEnd/src/utilities/hooks/useReverseScrollPositionRemember.ts deleted file mode 100644 index 339a12b8..00000000 --- a/FrontEnd/src/utilities/hooks/useReverseScrollPositionRemember.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Not used now!!! But preserved for future use. - -import { useEffect } from "react"; - -let on = false; - -let rememberedReversePosition = getReverseScrollPosition(); - -export function getReverseScrollPosition(): number { - if (document.documentElement.scrollHeight <= window.innerHeight) { - return 0; - } else { - return ( - document.documentElement.scrollHeight - - document.documentElement.scrollTop - - window.innerHeight - ); - } -} - -export function scrollToReverseScrollPosition(reversePosition: number): void { - if (document.documentElement.scrollHeight <= window.innerHeight) return; - - const old = document.documentElement.style.scrollBehavior; - document.documentElement.style.scrollBehavior = "auto"; - - const newPosition = - document.documentElement.scrollHeight - - window.innerHeight - - reversePosition; - - window.scrollTo(0, newPosition); - - document.documentElement.style.scrollBehavior = old; -} - -const scrollListener = (): void => { - rememberedReversePosition = getReverseScrollPosition(); -}; - -const resizeObserver = new ResizeObserver(() => { - scrollToReverseScrollPosition(rememberedReversePosition); -}); - -export default function useReverseScrollPositionRemember(): void { - useEffect(() => { - if (on) return; - on = true; - window.addEventListener("scroll", scrollListener); - resizeObserver.observe(document.documentElement); - - return () => { - resizeObserver.disconnect(); - window.removeEventListener("scroll", scrollListener); - on = false; - }; - }, []); -} -- cgit v1.2.3