fix: rerender i18n in host components on lang change (#6224)
This commit is contained in:
parent
e4506be3e8
commit
04a8c22f39
@ -1,5 +1,5 @@
|
||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||
import { t } from "../../i18n";
|
||||
import { useI18n } from "../../i18n";
|
||||
import {
|
||||
useExcalidrawAppState,
|
||||
useExcalidrawSetAppState,
|
||||
@ -33,9 +33,7 @@ import { useSetAtom } from "jotai";
|
||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||
|
||||
export const LoadScene = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
||||
@ -57,9 +55,7 @@ export const LoadScene = () => {
|
||||
LoadScene.displayName = "LoadScene";
|
||||
|
||||
export const SaveToActiveFile = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
|
||||
@ -80,9 +76,7 @@ SaveToActiveFile.displayName = "SaveToActiveFile";
|
||||
|
||||
export const SaveAsImage = () => {
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={ExportImageIcon}
|
||||
@ -98,9 +92,7 @@ export const SaveAsImage = () => {
|
||||
SaveAsImage.displayName = "SaveAsImage";
|
||||
|
||||
export const Help = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
@ -119,9 +111,8 @@ export const Help = () => {
|
||||
Help.displayName = "Help";
|
||||
|
||||
export const ClearCanvas = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
|
||||
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
@ -143,6 +134,7 @@ export const ClearCanvas = () => {
|
||||
ClearCanvas.displayName = "ClearCanvas";
|
||||
|
||||
export const ToggleTheme = () => {
|
||||
const { t } = useI18n();
|
||||
const appState = useExcalidrawAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
@ -175,6 +167,7 @@ export const ToggleTheme = () => {
|
||||
ToggleTheme.displayName = "ToggleTheme";
|
||||
|
||||
export const ChangeCanvasBackground = () => {
|
||||
const { t } = useI18n();
|
||||
const appState = useExcalidrawAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
@ -195,9 +188,7 @@ export const ChangeCanvasBackground = () => {
|
||||
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
|
||||
|
||||
export const Export = () => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
@ -248,9 +239,7 @@ export const LiveCollaborationTrigger = ({
|
||||
onSelect: () => void;
|
||||
isCollaborating: boolean;
|
||||
}) => {
|
||||
// FIXME Hack until we tie "t" to lang state
|
||||
// eslint-disable-next-line
|
||||
const appState = useExcalidrawAppState();
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
data-testid="collab-button"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { actionLoadScene, actionShortcuts } from "../../actions";
|
||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||
import { t } from "../../i18n";
|
||||
import { t, useI18n } from "../../i18n";
|
||||
import {
|
||||
useDevice,
|
||||
useExcalidrawActionManager,
|
||||
@ -172,10 +172,7 @@ const MenuItemLiveCollaborationTrigger = ({
|
||||
}: {
|
||||
onSelect: () => any;
|
||||
}) => {
|
||||
// FIXME when we tie t() to lang state
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const appState = useExcalidrawAppState();
|
||||
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
|
||||
{t("labels.liveCollaboration")}
|
||||
|
3
src/excalidraw-app/app-jotai.ts
Normal file
3
src/excalidraw-app/app-jotai.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { unstable_createStore } from "jotai";
|
||||
|
||||
export const appJotaiStore = unstable_createStore();
|
@ -70,7 +70,7 @@ import { decryptData } from "../../data/encryption";
|
||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||
import { LocalData } from "../data/LocalData";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { jotaiStore } from "../../jotai";
|
||||
import { appJotaiStore } from "../app-jotai";
|
||||
|
||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||
export const collabDialogShownAtom = atom(false);
|
||||
@ -167,7 +167,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
setUsername: this.setUsername,
|
||||
};
|
||||
|
||||
jotaiStore.set(collabAPIAtom, collabAPI);
|
||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||
this.onOfflineStatusToggle();
|
||||
|
||||
if (
|
||||
@ -185,7 +185,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
|
||||
onOfflineStatusToggle = () => {
|
||||
jotaiStore.set(isOfflineAtom, !window.navigator.onLine);
|
||||
appJotaiStore.set(isOfflineAtom, !window.navigator.onLine);
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -208,10 +208,10 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
}
|
||||
|
||||
isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!;
|
||||
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
|
||||
|
||||
private setIsCollaborating = (isCollaborating: boolean) => {
|
||||
jotaiStore.set(isCollaboratingAtom, isCollaborating);
|
||||
appJotaiStore.set(isCollaboratingAtom, isCollaborating);
|
||||
};
|
||||
|
||||
private onUnload = () => {
|
||||
@ -804,7 +804,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
);
|
||||
|
||||
handleClose = () => {
|
||||
jotaiStore.set(collabDialogShownAtom, false);
|
||||
appJotaiStore.set(collabDialogShownAtom, false);
|
||||
};
|
||||
|
||||
setUsername = (username: string) => {
|
||||
|
@ -10,13 +10,13 @@ import {
|
||||
shareWindows,
|
||||
} from "../../components/icons";
|
||||
import { ToolButton } from "../../components/ToolButton";
|
||||
import { t } from "../../i18n";
|
||||
import "./RoomDialog.scss";
|
||||
import Stack from "../../components/Stack";
|
||||
import { AppState } from "../../types";
|
||||
import { trackEvent } from "../../analytics";
|
||||
import { getFrame } from "../../utils";
|
||||
import DialogActionButton from "../../components/DialogActionButton";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
||||
const getShareIcon = () => {
|
||||
const navigator = window.navigator as any;
|
||||
@ -51,6 +51,7 @@ const RoomDialog = ({
|
||||
setErrorMessage: (message: string) => void;
|
||||
theme: AppState["theme"];
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const copyRoomLink = async () => {
|
||||
|
@ -1,12 +1,13 @@
|
||||
import React from "react";
|
||||
import { PlusPromoIcon } from "../../components/icons";
|
||||
import { t } from "../../i18n";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
|
||||
export const AppWelcomeScreen: React.FC<{
|
||||
setCollabDialogShown: (toggle: boolean) => any;
|
||||
}> = React.memo((props) => {
|
||||
const { t } = useI18n();
|
||||
let headingContent;
|
||||
|
||||
if (isExcalidrawPlusSignedUser) {
|
||||
|
@ -1,17 +1,21 @@
|
||||
import { shield } from "../../components/icons";
|
||||
import { Tooltip } from "../../components/Tooltip";
|
||||
import { t } from "../../i18n";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
||||
export const EncryptedIcon = () => (
|
||||
<a
|
||||
className="encrypted-icon tooltip"
|
||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t("encrypted.link")}
|
||||
>
|
||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||
{shield}
|
||||
</Tooltip>
|
||||
</a>
|
||||
);
|
||||
export const EncryptedIcon = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<a
|
||||
className="encrypted-icon tooltip"
|
||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t("encrypted.link")}
|
||||
>
|
||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||
{shield}
|
||||
</Tooltip>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||
import { FileId, NonDeletedExcalidrawElement } from "../../element/types";
|
||||
import { AppState, BinaryFileData, BinaryFiles } from "../../types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { t } from "../../i18n";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { excalidrawPlusIcon } from "./icons";
|
||||
import { encryptData, generateEncryptionKey } from "../../data/encryption";
|
||||
import { isInitializedImageElement } from "../../element/typeChecks";
|
||||
@ -79,6 +79,7 @@ export const ExportToExcalidrawPlus: React.FC<{
|
||||
files: BinaryFiles;
|
||||
onError: (error: Error) => void;
|
||||
}> = ({ elements, appState, files, onError }) => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<Card color="primary">
|
||||
<div className="Card-icon">{excalidrawPlusIcon}</div>
|
||||
|
@ -1,22 +1,23 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { useSetAtom } from "jotai";
|
||||
import React from "react";
|
||||
import { langCodeAtom } from "..";
|
||||
import * as i18n from "../../i18n";
|
||||
import { appLangCodeAtom } from "..";
|
||||
import { defaultLang, useI18n } from "../../i18n";
|
||||
import { languages } from "../../i18n";
|
||||
|
||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||
const [langCode, setLangCode] = useAtom(langCodeAtom);
|
||||
const { t, langCode } = useI18n();
|
||||
const setLangCode = useSetAtom(appLangCodeAtom);
|
||||
|
||||
return (
|
||||
<select
|
||||
className="dropdown-select dropdown-select__language"
|
||||
onChange={({ target }) => setLangCode(target.value)}
|
||||
value={langCode}
|
||||
aria-label={i18n.t("buttons.selectLanguage")}
|
||||
aria-label={t("buttons.selectLanguage")}
|
||||
style={style}
|
||||
>
|
||||
<option key={i18n.defaultLang.code} value={i18n.defaultLang.code}>
|
||||
{i18n.defaultLang.label}
|
||||
<option key={defaultLang.code} value={defaultLang.code}>
|
||||
{defaultLang.label}
|
||||
</option>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
|
@ -75,13 +75,14 @@ import { loadFilesFromFirebase } from "./data/firebase";
|
||||
import { LocalData } from "./data/LocalData";
|
||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||
import clsx from "clsx";
|
||||
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
||||
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
||||
import { reconcileElements } from "./collab/reconciliation";
|
||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||
import { AppMainMenu } from "./components/AppMainMenu";
|
||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||
import { AppFooter } from "./components/AppFooter";
|
||||
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
||||
import { useAtomWithInitialValue } from "../jotai";
|
||||
import { appJotaiStore } from "./app-jotai";
|
||||
|
||||
import "./index.scss";
|
||||
|
||||
@ -226,15 +227,15 @@ const initializeScene = async (opts: {
|
||||
return { scene: null, isExternalScene: false };
|
||||
};
|
||||
|
||||
const currentLangCode = languageDetector.detect() || defaultLang.code;
|
||||
|
||||
export const langCodeAtom = atom(
|
||||
Array.isArray(currentLangCode) ? currentLangCode[0] : currentLangCode,
|
||||
const detectedLangCode = languageDetector.detect() || defaultLang.code;
|
||||
export const appLangCodeAtom = atom(
|
||||
Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
|
||||
);
|
||||
|
||||
const ExcalidrawWrapper = () => {
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [langCode, setLangCode] = useAtom(langCodeAtom);
|
||||
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||
|
||||
// initial state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -683,7 +684,7 @@ const ExcalidrawWrapper = () => {
|
||||
const ExcalidrawApp = () => {
|
||||
return (
|
||||
<TopErrorBoundary>
|
||||
<Provider unstable_createStore={() => jotaiStore}>
|
||||
<Provider unstable_createStore={() => appJotaiStore}>
|
||||
<ExcalidrawWrapper />
|
||||
</Provider>
|
||||
</TopErrorBoundary>
|
||||
|
16
src/i18n.ts
16
src/i18n.ts
@ -1,6 +1,8 @@
|
||||
import fallbackLangData from "./locales/en.json";
|
||||
import percentages from "./locales/percentages.json";
|
||||
import { ENV } from "./constants";
|
||||
import { jotaiScope, jotaiStore } from "./jotai";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
|
||||
const COMPLETION_THRESHOLD = 85;
|
||||
|
||||
@ -99,6 +101,8 @@ export const setLanguage = async (lang: Language) => {
|
||||
currentLangData = fallbackLangData;
|
||||
}
|
||||
}
|
||||
|
||||
jotaiStore.set(editorLangCodeAtom, lang.code);
|
||||
};
|
||||
|
||||
export const getLanguage = () => currentLang;
|
||||
@ -143,3 +147,15 @@ export const t = (
|
||||
}
|
||||
return translation;
|
||||
};
|
||||
|
||||
/** @private atom used solely to rerender components using `useI18n` hook */
|
||||
const editorLangCodeAtom = atom(defaultLang.code);
|
||||
|
||||
// Should be used in components that fall under these cases:
|
||||
// - component is rendered as an <Excalidraw> child
|
||||
// - component is rendered internally by <Excalidraw>, but the component
|
||||
// is memoized w/o being updated on `langCode`, `AppState`, or `UIAppState`
|
||||
export const useI18n = () => {
|
||||
const langCode = useAtomValue(editorLangCodeAtom, jotaiScope);
|
||||
return { t, langCode };
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { unstable_createStore, useAtom, WritableAtom } from "jotai";
|
||||
import { PrimitiveAtom, unstable_createStore, useAtom } from "jotai";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export const jotaiScope = Symbol();
|
||||
@ -6,7 +6,7 @@ export const jotaiStore = unstable_createStore();
|
||||
|
||||
export const useAtomWithInitialValue = <
|
||||
T extends unknown,
|
||||
A extends WritableAtom<T, T>,
|
||||
A extends PrimitiveAtom<T>,
|
||||
>(
|
||||
atom: A,
|
||||
initialValue: T | (() => T),
|
||||
|
@ -15,6 +15,8 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Features
|
||||
|
||||
- Expose `useI18n()` hook return an object containing `t()` i18n helper and current `langCode`. You can use this in components you render as `<Excalidraw>` children to render any of our i18n locale strings. [#6224](https://github.com/excalidraw/excalidraw/pull/6224)
|
||||
|
||||
- [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes
|
||||
|
||||
```js
|
||||
|
@ -87,8 +87,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InitializeApp langCode={langCode} theme={theme}>
|
||||
<Provider unstable_createStore={() => jotaiStore} scope={jotaiScope}>
|
||||
<Provider unstable_createStore={() => jotaiStore} scope={jotaiScope}>
|
||||
<InitializeApp langCode={langCode} theme={theme}>
|
||||
<App
|
||||
onChange={onChange}
|
||||
initialData={initialData}
|
||||
@ -118,8 +118,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
>
|
||||
{children}
|
||||
</App>
|
||||
</Provider>
|
||||
</InitializeApp>
|
||||
</InitializeApp>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -198,7 +198,7 @@ export {
|
||||
isInvisiblySmallElement,
|
||||
getNonDeletedElements,
|
||||
} from "../../element";
|
||||
export { defaultLang, languages } from "../../i18n";
|
||||
export { defaultLang, useI18n, languages } from "../../i18n";
|
||||
export {
|
||||
restore,
|
||||
restoreAppState,
|
||||
|
Loading…
x
Reference in New Issue
Block a user