aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-09-21 23:49:12 +0800
committercrupest <crupest@outlook.com>2023-09-21 23:49:12 +0800
commitd477c7270c90b190ed82b13f48f39a05d83503d2 (patch)
tree572f74b2adc75247b9d1c9a8965e3de2559d4160 /FrontEnd/src
parenta8a8385cd959e4d9d57b8a35381d2851049c07ff (diff)
downloadtimeline-d477c7270c90b190ed82b13f48f39a05d83503d2.tar.gz
timeline-d477c7270c90b190ed82b13f48f39a05d83503d2.tar.bz2
timeline-d477c7270c90b190ed82b13f48f39a05d83503d2.zip
Fix #1394.
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/common.ts9
-rw-r--r--FrontEnd/src/components/AppBar.tsx3
-rw-r--r--FrontEnd/src/components/SearchInput.tsx4
-rw-r--r--FrontEnd/src/components/alert/AlertHost.tsx4
-rw-r--r--FrontEnd/src/components/alert/AlertService.ts4
-rw-r--r--FrontEnd/src/components/button/Button.tsx4
-rw-r--r--FrontEnd/src/components/button/ButtonRowV2.tsx10
-rw-r--r--FrontEnd/src/components/button/FlatButton.tsx4
-rw-r--r--FrontEnd/src/components/common.ts9
-rw-r--r--FrontEnd/src/components/dialog/ConfirmDialog.tsx6
-rw-r--r--FrontEnd/src/components/dialog/DialogContainer.tsx4
-rw-r--r--FrontEnd/src/components/dialog/OperationDialog.tsx14
-rw-r--r--FrontEnd/src/components/hooks/useWindowLeave.ts4
-rw-r--r--FrontEnd/src/components/input/InputGroup.tsx14
-rw-r--r--FrontEnd/src/components/menu/Menu.tsx4
-rw-r--r--FrontEnd/src/components/tab/TabBar.tsx4
-rw-r--r--FrontEnd/src/components/tab/TabPages.tsx4
-rw-r--r--FrontEnd/src/i18n.ts114
-rw-r--r--FrontEnd/src/i18n/backend.ts33
-rw-r--r--FrontEnd/src/i18n/index.ts3
-rw-r--r--FrontEnd/src/i18n/setup.ts50
-rw-r--r--FrontEnd/src/i18n/text.ts35
-rw-r--r--FrontEnd/src/i18n/translations/en/admin.json (renamed from FrontEnd/src/locales/en/admin.json)0
-rw-r--r--FrontEnd/src/i18n/translations/en/index.json (renamed from FrontEnd/src/locales/en/translation.json)0
-rw-r--r--FrontEnd/src/i18n/translations/zh/admin.json (renamed from FrontEnd/src/locales/zh/admin.json)0
-rw-r--r--FrontEnd/src/i18n/translations/zh/index.json (renamed from FrontEnd/src/locales/zh/translation.json)0
-rw-r--r--FrontEnd/src/migrating/hooks/useReverseScrollPositionRemember.ts (renamed from FrontEnd/src/utilities/hooks/useReverseScrollPositionRemember.ts)0
-rw-r--r--FrontEnd/src/pages/about/index.tsx2
-rw-r--r--FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx5
-rw-r--r--FrontEnd/src/pages/setting/index.tsx15
-rw-r--r--FrontEnd/src/pages/timeline/Timeline.tsx2
-rw-r--r--FrontEnd/src/pages/timeline/TimelineMember.tsx3
-rw-r--r--FrontEnd/src/pages/timeline/edit/TimelinePostCreateView.tsx4
-rw-r--r--FrontEnd/src/pages/timeline/index.tsx2
-rw-r--r--FrontEnd/src/services/alert.ts3
-rw-r--r--FrontEnd/src/services/user.ts2
-rw-r--r--FrontEnd/src/utilities/hooks/use-c.ts7
37 files changed, 187 insertions, 198 deletions
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)}
</NavLink>
);
}
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<HTMLButtonElement> | 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<typeof Button>;
}
interface ButtonRowV2NormalButton extends ButtonRowV2ButtonBase {
type: "normal";
- text: Text;
+ text: I18nText;
outline?: boolean;
props?: ComponentPropsWithoutRef<typeof Button>;
}
interface ButtonRowV2FlatButton extends ButtonRowV2ButtonBase {
type: "flat";
- text: Text;
+ text: I18nText;
props?: ComponentPropsWithoutRef<typeof FlatButton>;
}
@@ -48,7 +48,7 @@ interface ButtonRowV2IconButton extends ButtonRowV2ButtonBase {
interface ButtonRowV2LoadingButton extends ButtonRowV2ButtonBase {
type: "loading";
- text: Text;
+ text: I18nText;
loading?: boolean;
props?: ComponentPropsWithoutRef<typeof LoadingButton>;
}
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<HTMLButtonElement> | 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<HTMLDivElement>;
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<TData> {
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<string, InputValue>;
-export type InputErrorDict = Record<string, Text>;
+export type InputErrorDict = Record<string, I18nText>;
export type InputDisabledDict = Record<string, boolean>;
export type InputDirtyDict = Record<string, boolean>;
// use never so you don't have to cast everywhere
export type InputConfirmValueDict = Record<string, never>;
export type GeneralInputErrorDict = {
- [key: string]: Text | null | undefined;
+ [key: string]: I18nText | null | undefined;
};
type MakeInputInfo<I extends Input> = Omit<I, "value" | "error" | "disabled">;
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<HTMLButtonElement>) => 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/locales/en/admin.json b/FrontEnd/src/i18n/translations/en/admin.json
index ddb3ffad..ddb3ffad 100644
--- a/FrontEnd/src/locales/en/admin.json
+++ b/FrontEnd/src/i18n/translations/en/admin.json
diff --git a/FrontEnd/src/locales/en/translation.json b/FrontEnd/src/i18n/translations/en/index.json
index 1b43357c..1b43357c 100644
--- a/FrontEnd/src/locales/en/translation.json
+++ b/FrontEnd/src/i18n/translations/en/index.json
diff --git a/FrontEnd/src/locales/zh/admin.json b/FrontEnd/src/i18n/translations/zh/admin.json
index edd1cabd..edd1cabd 100644
--- a/FrontEnd/src/locales/zh/admin.json
+++ b/FrontEnd/src/i18n/translations/zh/admin.json
diff --git a/FrontEnd/src/locales/zh/translation.json b/FrontEnd/src/i18n/translations/zh/index.json
index dc0d6672..dc0d6672 100644
--- a/FrontEnd/src/locales/zh/translation.json
+++ b/FrontEnd/src/i18n/translations/zh/index.json
diff --git a/FrontEnd/src/utilities/hooks/useReverseScrollPositionRemember.ts b/FrontEnd/src/migrating/hooks/useReverseScrollPositionRemember.ts
index 339a12b8..339a12b8 100644
--- a/FrontEnd/src/utilities/hooks/useReverseScrollPositionRemember.ts
+++ b/FrontEnd/src/migrating/hooks/useReverseScrollPositionRemember.ts
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<Blob | null>(null);
- const [message, setMessage] = useState<Text>(
+ const [message, setMessage] = useState<I18nText>(
"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<ComponentPropsWithoutRef<typeof Card>, "title"> {
- title: Text;
+ title: I18nText;
children?: ReactNode;
}
@@ -49,8 +48,8 @@ function SettingSection({
interface SettingItemContainerProps
extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
- title: Text;
- description?: Text;
+ title: I18nText;
+ description?: I18nText;
danger?: boolean;
extraClassName?: string;
}
@@ -78,7 +77,9 @@ function SettingItemContainer({
>
<div className="setting-item-label-area">
<div className="setting-item-label-title">{c(title)}</div>
- <small className="setting-item-label-sub">{c(description)}</small>
+ {description && (
+ <small className="setting-item-label-sub">{c(description)}</small>
+ )}
</div>
<div className="setting-item-value-area">{children}</div>
</div>
@@ -97,7 +98,7 @@ interface SelectSettingItemProps
extends Omit<SettingItemContainerProps, "onSelect" | "extraClassName"> {
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);
-}