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:
parent
637276301a
commit
e4919e2e6c
47
package-lock.json
generated
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
|
||||
|
92
src/i18n.ts
92
src/i18n.ts
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user