ソースを参照

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)
Christopher Chedeau 5 年 前
コミット
e4919e2e6c

+ 0 - 47
package-lock.json

@@ -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",

+ 0 - 4
package.json

@@ -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",

+ 3 - 2
src/actions/actionCanvas.tsx

@@ -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}

+ 5 - 4
src/actions/actionExport.tsx

@@ -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}

+ 9 - 8
src/actions/actionProperties.tsx

@@ -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

+ 2 - 8
src/actions/manager.tsx

@@ -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}
         />
       );
     }

+ 0 - 3
src/actions/types.ts

@@ -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;
 }

+ 1 - 7
src/components/ColorPicker.tsx

@@ -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}

+ 1 - 5
src/components/ExportDialog.tsx

@@ -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);
 

+ 4 - 6
src/components/LanguageList.tsx

@@ -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")}
       >

+ 0 - 4
src/components/StoredScenesList.test.tsx

@@ -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 - 3
src/components/StoredScenesList.tsx

@@ -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

+ 61 - 29
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 const languages = [
+  { 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") },
+];
+
+let currentLanguage = languages[0];
+const fallbackLanguage = languages[0];
+
+export function setLanguage(newLng: string | undefined) {
+  currentLanguage =
+    languages.find(language => language.lng === newLng) || fallbackLanguage;
+
+  languageDetector.cacheUserLanguage(currentLanguage.lng);
+}
+
+export function getLanguage() {
+  return currentLanguage.lng;
+}
 
-export function parseDetectedLang(lng: string | undefined): string {
-  if (lng) {
-    const [lang] = i18n.language.split("-");
-    return lang;
+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];
   }
-  return fallbackLng;
+  if (typeof data !== "string") {
+    return undefined;
+  }
+  return data;
 }
 
-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: "Русский" },
-];
+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,
+});
 
-i18n
-  .use(Backend)
-  .use(LanguageDetector)
-  .use(initReactI18next)
-  .init({
-    fallbackLng,
-    react: { useSuspense: false },
-    load: "languageOnly",
-  });
-
-export default i18n;
+setLanguage(languageDetector.detect());

+ 6 - 28
src/index.tsx

@@ -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,
 );

+ 0 - 0
public/locales/de/translation.json → src/locales/de.json


+ 0 - 0
public/locales/en/translation.json → src/locales/en.json


+ 0 - 0
public/locales/es/translation.json → src/locales/es.json


+ 0 - 0
public/locales/fr/translation.json → src/locales/fr.json


+ 0 - 0
public/locales/pt/translation.json → src/locales/pt.json


+ 0 - 0
public/locales/ru/translation.json → src/locales/ru.json


+ 7 - 8
src/scene/data.ts

@@ -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 {