1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2025-02-18 13:29:36 +01:00

Replace i18n by a custom implementation (#638)

There are two problems with the current localization strategy:
- We download the translations on-demand, which means that it does a serial roundtrip for nothing.
- withTranslation helper actually renders the app 3 times on startup, instead of once (I haven't tried to debug it)
This commit is contained in:
Christopher Chedeau 2020-01-31 21:06:06 +00:00 committed by GitHub
parent 637276301a
commit e4919e2e6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 101 additions and 167 deletions

47
package-lock.json generated

@ -1681,15 +1681,6 @@
"csstype": "^2.2.0"
}
},
"@types/react-color": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.1.tgz",
"integrity": "sha512-J6mYm43Sid9y+OjZ7NDfJ2VVkeeuTPNVImNFITgQNXodHteKfl/t/5pAR5Z9buodZ2tCctsZjgiMlQOpfntakw==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-dom": {
"version": "16.9.5",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz",
@ -7123,14 +7114,6 @@
}
}
},
"html-parse-stringify2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz",
"integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=",
"requires": {
"void-elements": "^2.0.1"
}
},
"html-webpack-plugin": {
"version": "4.0.0-beta.5",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.5.tgz",
@ -7412,14 +7395,6 @@
}
}
},
"i18next": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.1.0.tgz",
"integrity": "sha512-ISbmukX4L6Dz0QoH9+EW1AnBw7j+NRLoMu9uLPMaNSSTP9Eie9/oUL0dOyWX15baB3gYOpkHJpGZRHOqcnl0ew==",
"requires": {
"@babel/runtime": "^7.3.1"
}
},
"i18next-browser-languagedetector": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.0.1.tgz",
@ -7428,14 +7403,6 @@
"@babel/runtime": "^7.5.5"
}
},
"i18next-xhr-backend": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/i18next-xhr-backend/-/i18next-xhr-backend-3.2.2.tgz",
"integrity": "sha512-OtRf2Vo3IqAxsttQbpjYnmMML12IMB5e0fc5B7qKJFLScitYaXa1OhMX0n0X/3vrfFlpHL9Ro/H+ps4Ej2j7QQ==",
"requires": {
"@babel/runtime": "^7.5.5"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -12928,15 +12895,6 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.4.tgz",
"integrity": "sha512-ueZzLmHltszTshDMwyfELDq8zOA803wQ1ZuzCccXa1m57k1PxSHfflPD5W9YIiTXLs0JTLzoj6o1LuM5N6zzNA=="
},
"react-i18next": {
"version": "11.3.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.3.1.tgz",
"integrity": "sha512-S/CWHcnew1lXo8HeniGhBU5kTmPhZ4w4rtA4m/gDN07soCtKKYSAcLNm7zhwjI2OSR4Skd0vOtzNp/FzEEjxIw==",
"requires": {
"@babel/runtime": "^7.3.1",
"html-parse-stringify2": "2.0.1"
}
},
"react-is": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
@ -15969,11 +15927,6 @@
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
},
"void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w="
},
"w3c-hr-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz",

@ -7,13 +7,10 @@
],
"dependencies": {
"browser-nativefs": "0.2.0",
"i18next": "19.1.0",
"i18next-browser-languagedetector": "4.0.1",
"i18next-xhr-backend": "3.2.2",
"nanoid": "2.1.10",
"react": "16.12.0",
"react-dom": "16.12.0",
"react-i18next": "11.3.1",
"react-scripts": "3.3.0",
"roughjs": "4.0.4"
},
@ -24,7 +21,6 @@
"@types/jest": "25.1.0",
"@types/nanoid": "2.1.0",
"@types/react": "16.9.19",
"@types/react-color": "3.0.1",
"@types/react-dom": "16.9.5",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.2",

@ -4,13 +4,14 @@ import { ColorPicker } from "../components/ColorPicker";
import { getDefaultAppState } from "../appState";
import { trash } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
export const actionChangeViewBackgroundColor: Action = {
name: "changeViewBackgroundColor",
perform: (elements, appState, value) => {
return { appState: { ...appState, viewBackgroundColor: value } };
},
PanelComponent: ({ appState, updateData, t }) => {
PanelComponent: ({ appState, updateData }) => {
return (
<div style={{ position: "relative" }}>
<ColorPicker
@ -32,7 +33,7 @@ export const actionClearCanvas: Action = {
appState: getDefaultAppState(),
};
},
PanelComponent: ({ updateData, t }) => (
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
icon={trash}

@ -4,13 +4,14 @@ import { ProjectName } from "../components/ProjectName";
import { saveAsJSON, loadFromJSON } from "../scene";
import { load, save } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
export const actionChangeProjectName: Action = {
name: "changeProjectName",
perform: (elements, appState, value) => {
return { appState: { ...appState, name: value } };
},
PanelComponent: ({ appState, updateData, t }) => (
PanelComponent: ({ appState, updateData }) => (
<ProjectName
label={t("labels.fileTitle")}
value={appState.name || "Unnamed"}
@ -24,7 +25,7 @@ export const actionChangeExportBackground: Action = {
perform: (elements, appState, value) => {
return { appState: { ...appState, exportBackground: value } };
},
PanelComponent: ({ appState, updateData, t }) => (
PanelComponent: ({ appState, updateData }) => (
<label>
<input
type="checkbox"
@ -44,7 +45,7 @@ export const actionSaveScene: Action = {
saveAsJSON(elements, appState).catch(err => console.error(err));
return {};
},
PanelComponent: ({ updateData, t }) => (
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
icon={save}
@ -64,7 +65,7 @@ export const actionLoadScene: Action = {
) => {
return { elements: loadedElements, appState: loadedAppState };
},
PanelComponent: ({ updateData, t }) => (
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
icon={load}

@ -6,6 +6,7 @@ import { ButtonSelect } from "../components/ButtonSelect";
import { isTextElement, redrawTextBoundingBox } from "../element";
import { ColorPicker } from "../components/ColorPicker";
import { AppState } from "../../src/types";
import { t } from "../i18n";
const changeProperty = (
elements: readonly ExcalidrawElement[],
@ -46,7 +47,7 @@ export const actionChangeStrokeColor: Action = {
appState: { ...appState, currentItemStrokeColor: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<>
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
<ColorPicker
@ -76,7 +77,7 @@ export const actionChangeBackgroundColor: Action = {
appState: { ...appState, currentItemBackgroundColor: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<>
<h3 aria-hidden="true">{t("labels.background")}</h3>
<ColorPicker
@ -106,7 +107,7 @@ export const actionChangeFillStyle: Action = {
appState: { ...appState, currentItemFillStyle: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.fill")}</legend>
<ButtonSelect
@ -142,7 +143,7 @@ export const actionChangeStrokeWidth: Action = {
appState: { ...appState, currentItemStrokeWidth: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.strokeWidth")}</legend>
<ButtonSelect
@ -176,7 +177,7 @@ export const actionChangeSloppiness: Action = {
appState: { ...appState, currentItemRoughness: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.sloppiness")}</legend>
<ButtonSelect
@ -210,7 +211,7 @@ export const actionChangeOpacity: Action = {
appState: { ...appState, currentItemOpacity: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<label className="control-label">
{t("labels.opacity")}
<input
@ -256,7 +257,7 @@ export const actionChangeFontSize: Action = {
},
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.fontSize")}</legend>
<ButtonSelect
@ -304,7 +305,7 @@ export const actionChangeFontFamily: Action = {
},
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.fontFamily")}</legend>
<ButtonSelect

@ -7,7 +7,7 @@ import {
} from "./types";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { TFunction } from "i18next";
import { t } from "../i18n";
export class ActionManager implements ActionsManagerInterface {
actions: { [keyProp: string]: Action } = {};
@ -48,7 +48,6 @@ export class ActionManager implements ActionsManagerInterface {
appState: AppState,
updater: UpdaterFn,
actionFilter: ActionFilterFn = action => action,
t?: TFunction,
) {
return Object.values(this.actions)
.filter(actionFilter)
@ -59,10 +58,7 @@ export class ActionManager implements ActionsManagerInterface {
(b.contextMenuOrder !== undefined ? b.contextMenuOrder : 999),
)
.map(action => ({
label:
t && action.contextItemLabel
? t(action.contextItemLabel)
: action.contextItemLabel!,
label: action.contextItemLabel ? t(action.contextItemLabel) : "",
action: () => {
updater(action.perform(elements, appState, null));
},
@ -74,7 +70,6 @@ export class ActionManager implements ActionsManagerInterface {
elements: readonly ExcalidrawElement[],
appState: AppState,
updater: UpdaterFn,
t: TFunction,
) {
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
const action = this.actions[name];
@ -88,7 +83,6 @@ export class ActionManager implements ActionsManagerInterface {
elements={elements}
appState={appState}
updateData={updateData}
t={t}
/>
);
}

@ -1,7 +1,6 @@
import React from "react";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { TFunction } from "i18next";
export type ActionResult = {
elements?: ExcalidrawElement[];
@ -23,7 +22,6 @@ export interface Action {
elements: readonly ExcalidrawElement[];
appState: AppState;
updateData: (formData: any) => void;
t: TFunction;
}>;
perform: ActionFn;
keyPriority?: number;
@ -57,6 +55,5 @@ export interface ActionsManagerInterface {
elements: readonly ExcalidrawElement[],
appState: AppState,
updater: UpdaterFn,
t: TFunction,
) => React.ReactElement | null;
}

@ -3,8 +3,7 @@ import { Popover } from "./Popover";
import "./ColorPicker.css";
import { KEYS } from "../keys";
import { useTranslation } from "react-i18next";
import { TFunction } from "i18next";
import { t } from "../i18n";
// This is a narrow reimplementation of the awesome react-color Twitter component
// https://github.com/casesandberg/react-color/blob/master/src/components/twitter/Twitter.js
@ -15,14 +14,12 @@ const Picker = function({
onChange,
onClose,
label,
t,
}: {
colors: string[];
color: string | null;
onChange: (color: string) => void;
onClose: () => void;
label: string;
t: TFunction;
}) {
const firstItem = React.useRef<HTMLButtonElement>();
const colorInput = React.useRef<HTMLInputElement>();
@ -158,8 +155,6 @@ export function ColorPicker({
onChange: (color: string) => void;
label: string;
}) {
const { t } = useTranslation();
const [isActive, setActive] = React.useState(false);
const pickerButton = React.useRef<HTMLButtonElement>(null);
@ -195,7 +190,6 @@ export function ColorPicker({
pickerButton.current?.focus();
}}
label={label}
t={t}
/>
</Popover>
) : null}

@ -11,8 +11,8 @@ import { AppState } from "../types";
import { exportToCanvas } from "../scene/export";
import { ActionsManagerInterface, UpdaterFn } from "../actions/types";
import Stack from "./Stack";
import { t } from "../i18n";
import { useTranslation } from "react-i18next";
import { KEYS } from "../keys";
const probablySupportsClipboard =
@ -52,7 +52,6 @@ function ExportModal({
onExportToBackend: ExportCB;
onCloseRequest: () => void;
}) {
const { t } = useTranslation();
const someElementIsSelected = elements.some(element => element.isSelected);
const [scale, setScale] = useState(defaultScale);
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
@ -170,7 +169,6 @@ function ExportModal({
elements,
appState,
syncActionResult,
t,
)}
<Stack.Col gap={1}>
<div className="ExportDialog__scales">
@ -195,7 +193,6 @@ function ExportModal({
elements,
appState,
syncActionResult,
t,
)}
{someElementIsSelected && (
<div>
@ -238,7 +235,6 @@ export function ExportDialog({
onExportToClipboard: ExportCB;
onExportToBackend: ExportCB;
}) {
const { t } = useTranslation();
const [modalIsShown, setModalIsShown] = useState(false);
const triggerButton = useRef<HTMLButtonElement>(null);

@ -1,22 +1,20 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { t } from "../i18n";
export function LanguageList<T>({
onClick,
onChange,
languages,
currentLanguage,
}: {
languages: { lng: string; label: string }[];
onClick: (value: string) => void;
onChange: (value: string) => void;
currentLanguage: string;
}) {
const { t } = useTranslation();
return (
<React.Fragment>
<select
className="language-select"
onChange={({ target }) => onClick(target.value)}
onChange={({ target }) => onChange(target.value)}
value={currentLanguage}
aria-label={t("buttons.selectLanguage")}
>

@ -6,10 +6,6 @@ import { PreviousScene } from "../scene/types";
Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: any) => key }),
}));
function setup(props: any) {
const currentProps = {
...props,

@ -1,6 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { PreviousScene } from "../scene/types";
import { t } from "../i18n";
interface StoredScenesListProps {
scenes: PreviousScene[];
@ -13,8 +13,6 @@ export function StoredScenesList({
currentId,
onChange,
}: StoredScenesListProps) {
const { t } = useTranslation();
return (
<React.Fragment>
<select

@ -1,36 +1,68 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-xhr-backend";
import LanguageDetector from "i18next-browser-languagedetector";
export const fallbackLng = "en";
export function parseDetectedLang(lng: string | undefined): string {
if (lng) {
const [lang] = i18n.language.split("-");
return lang;
}
return fallbackLng;
}
export const languages = [
{ lng: "de", label: "Deutsch" },
{ lng: "en", label: "English" },
{ lng: "es", label: "Español" },
{ lng: "fr", label: "Français" },
{ lng: "pt", label: "Português" },
{ lng: "ru", label: "Русский" },
{ lng: "en", label: "English", data: require("./locales/en.json") },
{ lng: "de", label: "Deutsch", data: require("./locales/de.json") },
{ lng: "es", label: "Español", data: require("./locales/es.json") },
{ lng: "fr", label: "Français", data: require("./locales/fr.json") },
{ lng: "pt", label: "Português", data: require("./locales/pt.json") },
{ lng: "ru", label: "Русский", data: require("./locales/ru.json") },
];
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng,
react: { useSuspense: false },
load: "languageOnly",
});
let currentLanguage = languages[0];
const fallbackLanguage = languages[0];
export default i18n;
export function setLanguage(newLng: string | undefined) {
currentLanguage =
languages.find(language => language.lng === newLng) || fallbackLanguage;
languageDetector.cacheUserLanguage(currentLanguage.lng);
}
export function getLanguage() {
return currentLanguage.lng;
}
function findPartsForData(data: any, parts: string[]) {
for (var i = 0; i < parts.length; ++i) {
const part = parts[i];
if (data[part] === undefined) {
return undefined;
}
data = data[part];
}
if (typeof data !== "string") {
return undefined;
}
return data;
}
export function t(path: string, replacement?: { [key: string]: string }) {
const parts = path.split(".");
let translation =
findPartsForData(currentLanguage.data, parts) ||
findPartsForData(fallbackLanguage.data, parts);
if (translation === undefined) {
throw new Error("Can't find translation for " + path);
}
if (replacement) {
for (var key in replacement) {
translation = translation.replace("{{" + key + "}}", replacement[key]);
}
}
return translation;
}
const languageDetector = new LanguageDetector();
languageDetector.init({
languageUtils: {
formatLanguageCode: function(lng: string) {
return lng;
},
isWhitelisted: () => true,
},
checkWhitelist: false,
});
setLanguage(languageDetector.detect());

@ -85,9 +85,8 @@ import { FixedSideContainer } from "./components/FixedSideContainer";
import { ToolButton } from "./components/ToolButton";
import { LockIcon } from "./components/LockIcon";
import { ExportDialog } from "./components/ExportDialog";
import { withTranslation } from "react-i18next";
import { LanguageList } from "./components/LanguageList";
import i18n, { languages, parseDetectedLang } from "./i18n";
import { t, languages, setLanguage, getLanguage } from "./i18n";
import { StoredScenesList } from "./components/StoredScenesList";
let { elements } = createScene();
@ -448,7 +447,6 @@ export class App extends React.Component<any, AppState> {
};
private renderSelectedShapeActions(elements: readonly ExcalidrawElement[]) {
const { t } = this.props;
const { elementType, editingElement } = this.state;
const targetElements = editingElement
? [editingElement]
@ -465,7 +463,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{(hasBackground(elementType) ||
targetElements.some(element => hasBackground(element.type))) && (
@ -475,7 +472,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{this.actionManager.renderAction(
@ -483,7 +479,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</>
)}
@ -496,7 +491,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{this.actionManager.renderAction(
@ -504,7 +498,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</>
)}
@ -517,7 +510,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{this.actionManager.renderAction(
@ -525,7 +517,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</>
)}
@ -535,7 +526,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{this.actionManager.renderAction(
@ -543,7 +533,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</div>
</Island>
@ -551,8 +540,6 @@ export class App extends React.Component<any, AppState> {
}
private renderShapesSwitcher() {
const { t } = this.props;
return (
<>
{SHAPES.map(({ value, icon }, index) => {
@ -584,7 +571,6 @@ export class App extends React.Component<any, AppState> {
}
private renderCanvasActions() {
const { t } = this.props;
return (
<Stack.Col gap={4}>
<Stack.Row justifyContent={"space-between"}>
@ -593,14 +579,12 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{this.actionManager.renderAction(
"saveScene",
elements,
this.state,
this.syncActionResult,
t,
)}
<ExportDialog
elements={elements}
@ -653,7 +637,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</Stack.Row>
{this.actionManager.renderAction(
@ -661,7 +644,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</Stack.Col>
);
@ -670,7 +652,6 @@ export class App extends React.Component<any, AppState> {
public render() {
const canvasWidth = window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT;
const canvasHeight = window.innerHeight - CANVAS_WINDOW_OFFSET_TOP;
const { t } = this.props;
return (
<div className="container">
@ -779,7 +760,6 @@ export class App extends React.Component<any, AppState> {
this.state,
this.syncActionResult,
action => this.canvasOnlyActions.includes(action),
t,
),
],
top: e.clientY,
@ -809,7 +789,6 @@ export class App extends React.Component<any, AppState> {
this.state,
this.syncActionResult,
action => !this.canvasOnlyActions.includes(action),
t,
),
],
top: e.clientY,
@ -1480,11 +1459,12 @@ export class App extends React.Component<any, AppState> {
</main>
<footer role="contentinfo">
<LanguageList
onClick={lng => {
i18n.changeLanguage(lng);
onChange={lng => {
setLanguage(lng);
this.setState({});
}}
languages={languages}
currentLanguage={parseDetectedLang(i18n.language)}
currentLanguage={getLanguage()}
/>
{this.renderIdsDropdown()}
</footer>
@ -1614,8 +1594,6 @@ export class App extends React.Component<any, AppState> {
}
}
const AppWithTrans = withTranslation()(App);
const rootElement = document.getElementById("root");
class TopErrorBoundary extends React.Component {
@ -1710,7 +1688,7 @@ class TopErrorBoundary extends React.Component {
ReactDOM.render(
<TopErrorBoundary>
<AppWithTrans />
<App />
</TopErrorBoundary>,
rootElement,
);

@ -9,7 +9,7 @@ import nanoid from "nanoid";
import { fileOpen, fileSave } from "browser-nativefs";
import { getCommonBounds } from "../element";
import i18n from "../i18n";
import { t } from "../i18n";
const LOCAL_STORAGE_KEY = "excalidraw";
const LOCAL_STORAGE_SCENE_PREVIOUS_KEY = "excalidraw-previos-scenes";
@ -142,16 +142,15 @@ export async function exportToBackend(
await navigator.clipboard.writeText(url.toString());
window.alert(
i18n.t("alerts.copiedToClipboard", {
t("alerts.copiedToClipboard", {
url: url.toString(),
interpolation: { escapeValue: false },
}),
);
} else {
window.alert(i18n.t("alerts.couldNotCreateShareableLink"));
window.alert(t("alerts.couldNotCreateShareableLink"));
}
} catch (e) {
window.alert(i18n.t("alerts.couldNotCreateShareableLink"));
window.alert(t("alerts.couldNotCreateShareableLink"));
return;
}
}
@ -167,7 +166,7 @@ export async function importFromBackend(id: string | null) {
elements = response.elements || elements;
appState = response.appState || appState;
} catch (error) {
window.alert(i18n.t("alerts.importBackendFailed"));
window.alert(t("alerts.importBackendFailed"));
console.error(error);
}
}
@ -193,7 +192,7 @@ export async function exportCanvas(
},
) {
if (!elements.length)
return window.alert(i18n.t("alerts.cannotExportEmptyCanvas"));
return window.alert(t("alerts.cannotExportEmptyCanvas"));
// calculate smallest area to fit the contents in
if (type === "svg") {
@ -227,7 +226,7 @@ export async function exportCanvas(
}
});
} else if (type === "clipboard") {
const errorMsg = i18n.t("alerts.couldNotCopyToClipboard");
const errorMsg = t("alerts.couldNotCopyToClipboard");
try {
tempCanvas.toBlob(async function(blob: any) {
try {