diff --git a/package-lock.json b/package-lock.json index dd3e52e9..09e322d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3054,8 +3054,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -3073,13 +3072,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3092,18 +3089,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -3206,8 +3200,7 @@ }, "inherits": { "version": "2.0.4", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -3217,7 +3210,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3230,20 +3222,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.9.0", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3260,7 +3249,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3341,8 +3329,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -3352,7 +3339,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3428,8 +3414,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -3459,7 +3444,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3477,7 +3461,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3516,13 +3499,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.1.1", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -6639,6 +6620,14 @@ } } }, + "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", @@ -6907,6 +6896,30 @@ } } }, + "i18next": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.0.3.tgz", + "integrity": "sha512-Ru4afr++b4cUApsIBifcMYyWG9Nx8wlFdq4DuOF+UuoPoQKfuh0iAVMekTjs6w1CZLUOVb5QZEuoYRLmu17EIA==", + "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", + "integrity": "sha512-RxSoX6mB8cab0CTIQ+klCS764vYRj+Jk621cnFVsINvcdlb/cdi3vQFyrPwmnowB7ReUadjHovgZX+RPIzHVQQ==", + "requires": { + "@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", @@ -7674,8 +7687,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -7693,13 +7705,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7712,18 +7722,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -7826,8 +7833,7 @@ }, "inherits": { "version": "2.0.4", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -7837,7 +7843,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7850,20 +7855,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.9.0", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7880,7 +7882,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -7961,8 +7962,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -7972,7 +7972,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -8048,8 +8047,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -8079,7 +8077,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8097,7 +8094,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8136,13 +8132,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.1.1", - "bundled": true, - "optional": true + "bundled": true } } } @@ -12324,6 +12318,15 @@ "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", @@ -15303,6 +15306,11 @@ "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", diff --git a/package.json b/package.json index 180bb67f..481957a3 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,13 @@ "not op_mini all" ], "dependencies": { + "i18next": "19.0.3", + "i18next-browser-languagedetector": "4.0.1", + "i18next-xhr-backend": "3.2.2", "nanoid": "2.1.9", "react": "16.12.0", "react-dom": "16.12.0", + "react-i18next": "11.3.1", "react-scripts": "3.3.0", "roughjs": "4.0.4" }, diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json new file mode 100644 index 00000000..b8d0076a --- /dev/null +++ b/public/locales/en/translation.json @@ -0,0 +1,47 @@ +{ + "labels": { + "paste": "Paste", + "selectAll": "Select All", + "copy": "Copy", + "bringForward": "Bring Forward", + "sendToBack": "Send To Back", + "bringToFront": "Bring To Front", + "sendBackward": "Send Backward", + "delete": "Delete", + "copyStyles": "Copy Styles", + "pasteStyles": "Paste Styles", + "stroke": "Stroke", + "background": "Background", + "fill": "Fill", + "strokeWidth": "Stroke Width", + "sloppiness": "Sloppiness", + "oppacity": "Oppacity", + "fontSize": "Font Size", + "fontFamily": "Font Family", + "onlySelected": "Only selected", + "withBackground": "With Background", + "handDrawn": "Hand-Drawn", + "normal": "Normal", + "code": "Code" + }, + "buttons": { + "clearReset": "Clear the canvas & reset background color", + "export": "Export", + "exportToPng": "Export to PNG", + "copyToClipboard": "Copy to clipboard", + "save": "Save", + "load": "Load" + }, + "alerts": { + "clearReset": "This will clear the whole canvas. Are you sure?" + }, + "toolBar": { + "selection": "Selection", + "rectangle": "Rectangle", + "diamond": "Diamond", + "ellipse": "Ellipse", + "arrow": "Arrow", + "line": "Line", + "text": "Text" + } +} diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx index 1ba56489..eb2bb72d 100644 --- a/src/actions/actionCanvas.tsx +++ b/src/actions/actionCanvas.tsx @@ -31,14 +31,14 @@ export const actionClearCanvas: Action = { appState: getDefaultAppState() }; }, - PanelComponent: ({ updateData }) => ( + PanelComponent: ({ updateData, t }) => ( { - if (window.confirm("This will clear the whole canvas. Are you sure?")) { + if (window.confirm(t("alerts.clearReset"))) { // TODO: Defined globally, since file handles aren't yet serializable. // Once `FileSystemFileHandle` can be serialized, make this // part of `AppState`. diff --git a/src/actions/actionDeleteSelected.tsx b/src/actions/actionDeleteSelected.tsx index bb1dab71..22ad3cb9 100644 --- a/src/actions/actionDeleteSelected.tsx +++ b/src/actions/actionDeleteSelected.tsx @@ -9,7 +9,7 @@ export const actionDeleteSelected: Action = { elements: deleteSelectedElements(elements) }; }, - contextItemLabel: "Delete", + contextItemLabel: "labels.delete", contextMenuOrder: 3, keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE }; diff --git a/src/actions/actionExport.tsx b/src/actions/actionExport.tsx index 02ddaa03..74583b16 100644 --- a/src/actions/actionExport.tsx +++ b/src/actions/actionExport.tsx @@ -23,7 +23,7 @@ export const actionChangeExportBackground: Action = { perform: (elements, appState, value) => { return { appState: { ...appState, exportBackground: value } }; }, - PanelComponent: ({ appState, updateData }) => ( + PanelComponent: ({ appState, updateData, t }) => ( ) }; @@ -43,12 +43,12 @@ export const actionSaveScene: Action = { saveAsJSON(elements, appState).catch(err => console.error(err)); return {}; }, - PanelComponent: ({ updateData }) => ( + PanelComponent: ({ updateData, t }) => ( updateData(null)} /> ) @@ -63,12 +63,12 @@ export const actionLoadScene: Action = { ) => { return { elements: loadedElements, appState: loadedAppState }; }, - PanelComponent: ({ updateData }) => ( + PanelComponent: ({ updateData, t }) => ( { loadFromJSON() .then(({ elements, appState }) => { diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index 1f26e0c0..5cfa0963 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -30,19 +30,21 @@ export const actionChangeStrokeColor: Action = { appState: { ...appState, currentItemStrokeColor: value } }; }, - PanelComponent: ({ elements, appState, updateData }) => ( - <> -
Stroke
- element.strokeColor) || - appState.currentItemStrokeColor - } - onChange={updateData} - /> - - ) + PanelComponent: ({ elements, appState, updateData, t }) => { + return ( + <> +
{t("labels.stroke")}
+ element.strokeColor) || + appState.currentItemStrokeColor + } + onChange={updateData} + /> + + ); + } }; export const actionChangeBackgroundColor: Action = { @@ -57,9 +59,9 @@ export const actionChangeBackgroundColor: Action = { appState: { ...appState, currentItemBackgroundColor: value } }; }, - PanelComponent: ({ elements, appState, updateData }) => ( + PanelComponent: ({ elements, appState, updateData, t }) => ( <> -
Background
+
{t("labels.background")}
( + PanelComponent: ({ elements, updateData, t }) => ( <> -
Fill
+
{t("labels.fill")}
( + PanelComponent: ({ elements, appState, updateData, t }) => ( <> -
Stroke Width
+
{t("labels.strokeWidth")}
( + PanelComponent: ({ elements, appState, updateData, t }) => ( <> -
Sloppiness
+
{t("labels.sloppiness")}
( + PanelComponent: ({ elements, updateData, t }) => ( <> -
Opacity
+
{t("labels.oppacity")}
( + PanelComponent: ({ elements, updateData, t }) => ( <> -
Font size
+
{t("labels.fontSize")}
( + PanelComponent: ({ elements, updateData, t }) => ( <> -
Font family
+
{t("labels.fontFamily")}
({ ...elem, isSelected: true })) }; }, - contextItemLabel: "Select All", + contextItemLabel: "labels.selectAll", keyTest: event => event[KEYS.META] && event.code === "KeyA" }; diff --git a/src/actions/actionStyles.ts b/src/actions/actionStyles.ts index ca49e3ab..c7cf2045 100644 --- a/src/actions/actionStyles.ts +++ b/src/actions/actionStyles.ts @@ -13,7 +13,7 @@ export const actionCopyStyles: Action = { } return {}; }, - contextItemLabel: "Copy Styles", + contextItemLabel: "labels.copyStyles", keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyC", contextMenuOrder: 0 }; @@ -45,7 +45,7 @@ export const actionPasteStyles: Action = { }) }; }, - contextItemLabel: "Paste Styles", + contextItemLabel: "labels.pasteStyles", keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyV", contextMenuOrder: 1 }; diff --git a/src/actions/actionZindex.tsx b/src/actions/actionZindex.tsx index 668cb975..36359029 100644 --- a/src/actions/actionZindex.tsx +++ b/src/actions/actionZindex.tsx @@ -16,7 +16,7 @@ export const actionSendBackward: Action = { appState }; }, - contextItemLabel: "Send Backward", + contextItemLabel: "labels.sendBackward", keyPriority: 40, keyTest: event => event[KEYS.META] && event.shiftKey && event.altKey && event.code === "KeyB" @@ -30,7 +30,7 @@ export const actionBringForward: Action = { appState }; }, - contextItemLabel: "Bring Forward", + contextItemLabel: "labels.bringForward", keyPriority: 40, keyTest: event => event[KEYS.META] && event.shiftKey && event.altKey && event.code === "KeyF" @@ -44,7 +44,7 @@ export const actionSendToBack: Action = { appState }; }, - contextItemLabel: "Send to Back", + contextItemLabel: "labels.sendToBack", keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyB" }; @@ -56,6 +56,6 @@ export const actionBringToFront: Action = { appState }; }, - contextItemLabel: "Bring to Front", + contextItemLabel: "labels.bringToFront", keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyF" }; diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index cc204921..859d74b7 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -7,6 +7,7 @@ import { } from "./types"; import { ExcalidrawElement } from "../element/types"; import { AppState } from "../types"; +import { TFunction } from "i18next"; export class ActionManager implements ActionsManagerInterface { actions: { [keyProp: string]: Action } = {}; @@ -46,7 +47,8 @@ export class ActionManager implements ActionsManagerInterface { elements: readonly ExcalidrawElement[], appState: AppState, updater: UpdaterFn, - actionFilter: ActionFilterFn = action => action + actionFilter: ActionFilterFn = action => action, + t?: TFunction ) { return Object.values(this.actions) .filter(actionFilter) @@ -57,7 +59,10 @@ export class ActionManager implements ActionsManagerInterface { (b.contextMenuOrder !== undefined ? b.contextMenuOrder : 999) ) .map(action => ({ - label: action.contextItemLabel!, + label: + t && action.contextItemLabel + ? t(action.contextItemLabel) + : action.contextItemLabel!, action: () => { updater(action.perform(elements, appState, null)); } @@ -68,7 +73,8 @@ export class ActionManager implements ActionsManagerInterface { name: string, elements: readonly ExcalidrawElement[], appState: AppState, - updater: UpdaterFn + updater: UpdaterFn, + t: TFunction ) { if (this.actions[name] && "PanelComponent" in this.actions[name]) { const action = this.actions[name]; @@ -82,6 +88,7 @@ export class ActionManager implements ActionsManagerInterface { elements={elements} appState={appState} updateData={updateData} + t={t} /> ); } diff --git a/src/actions/types.ts b/src/actions/types.ts index b50e0a90..877b45d2 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -1,6 +1,7 @@ import React from "react"; import { ExcalidrawElement } from "../element/types"; import { AppState } from "../types"; +import { TFunction } from "i18next"; export type ActionResult = { elements?: ExcalidrawElement[]; @@ -22,6 +23,7 @@ export interface Action { elements: readonly ExcalidrawElement[]; appState: AppState; updateData: (formData: any) => void; + t: TFunction; }>; perform: ActionFn; keyPriority?: number; @@ -54,6 +56,7 @@ export interface ActionsManagerInterface { name: string, elements: readonly ExcalidrawElement[], appState: AppState, - updater: UpdaterFn + updater: UpdaterFn, + t: TFunction ) => React.ReactElement | null; } diff --git a/src/components/ExportDialog.tsx b/src/components/ExportDialog.tsx index 36dc587f..3b72a947 100644 --- a/src/components/ExportDialog.tsx +++ b/src/components/ExportDialog.tsx @@ -12,6 +12,8 @@ import { getExportCanvasPreview } from "../scene/getExportCanvasPreview"; import { ActionsManagerInterface, UpdaterFn } from "../actions/types"; import Stack from "./Stack"; +import { useTranslation } from "react-i18next"; + const probablySupportsClipboard = "toBlob" in HTMLCanvasElement.prototype && "clipboard" in navigator && @@ -42,6 +44,7 @@ export function ExportDialog({ onExportToClipboard: ExportCB; onExportToBackend: ExportCB; }) { + const { t } = useTranslation(); const someElementIsSelected = elements.some(element => element.isSelected); const [modalIsShown, setModalIsShown] = useState(false); const [scale, setScale] = useState(defaultScale); @@ -90,7 +93,7 @@ export function ExportDialog({ icon={exportFile} type="button" aria-label="Show export dialog" - title="Export" + title={t("buttons.export")} /> {modalIsShown && ( @@ -99,23 +102,23 @@ export function ExportDialog({ -

Export

+

{t("buttons.export")}

onExportToPng(exportedElements, scale)} /> {probablySupportsClipboard && ( onExportToClipboard(exportedElements, scale) } @@ -134,7 +137,8 @@ export function ExportDialog({ "changeProjectName", elements, appState, - syncActionResult + syncActionResult, + t )}
@@ -157,7 +161,8 @@ export function ExportDialog({ "changeExportBackground", elements, appState, - syncActionResult + syncActionResult, + t )} {someElementIsSelected && (
@@ -169,7 +174,7 @@ export function ExportDialog({ setExportSelected(e.currentTarget.checked) } />{" "} - Only selected + {t("labels.onlySelected")}
)} diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 00000000..2a6fcdc9 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,21 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; + +import Backend from "i18next-xhr-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + backend: { + loadPath: "./locales/{{lng}}/translation.json" + }, + lng: "en", + fallbackLng: "en", + debug: false, + react: { useSuspense: false } + }); + +export default i18n; diff --git a/src/index.tsx b/src/index.tsx index 9eccaf55..3dd69f03 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -77,6 +77,8 @@ import Stack from "./components/Stack"; import { FixedSideContainer } from "./components/FixedSideContainer"; import { ToolIcon } from "./components/ToolIcon"; import { ExportDialog } from "./components/ExportDialog"; +import { withTranslation } from "react-i18next"; +import "./i18n"; let { elements } = createScene(); const { history } = createHistory(); @@ -129,7 +131,7 @@ export function viewportCoordsToSceneCoords( return { x, y }; } -export class App extends React.Component<{}, AppState> { +export class App extends React.Component { canvas: HTMLCanvasElement | null = null; rc: RoughCanvas | null = null; @@ -359,6 +361,7 @@ export class App extends React.Component<{}, AppState> { }; private renderSelectedShapeActions(elements: readonly ExcalidrawElement[]) { + const { t } = this.props; const { elementType, editingElement } = this.state; const selectedElements = elements.filter(el => el.isSelected); const hasSelectedElements = selectedElements.length > 0; @@ -381,7 +384,8 @@ export class App extends React.Component<{}, AppState> { "changeStrokeColor", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )} {(hasBackground(elements) || @@ -391,14 +395,16 @@ export class App extends React.Component<{}, AppState> { "changeBackgroundColor", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )} {this.actionManager.renderAction( "changeFillStyle", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )}
@@ -411,14 +417,16 @@ export class App extends React.Component<{}, AppState> { "changeStrokeWidth", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )} {this.actionManager.renderAction( "changeSloppiness", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )}
@@ -430,14 +438,16 @@ export class App extends React.Component<{}, AppState> { "changeFontSize", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )} {this.actionManager.renderAction( "changeFontFamily", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )}
@@ -447,14 +457,16 @@ export class App extends React.Component<{}, AppState> { "changeOpacity", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )} {this.actionManager.renderAction( "deleteSelectedElements", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )}
@@ -462,32 +474,38 @@ export class App extends React.Component<{}, AppState> { } private renderShapesSwitcher() { + const { t } = this.props; + return ( <> - {SHAPES.map(({ value, icon }, index) => ( - { - this.setState({ elementType: value }); - elements = clearSelection(elements); - document.documentElement.style.cursor = - value === "text" ? "text" : "crosshair"; - this.forceUpdate(); - }} - > - ))} + {SHAPES.map(({ value, icon }, index) => { + const label = t(`toolBar.${value}`); + return ( + { + this.setState({ elementType: value }); + elements = clearSelection(elements); + document.documentElement.style.cursor = + value === "text" ? "text" : "crosshair"; + this.forceUpdate(); + }} + > + ); + })} ); } private renderCanvasActions() { + const { t } = this.props; return ( @@ -495,13 +513,15 @@ export class App extends React.Component<{}, AppState> { "loadScene", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )} {this.actionManager.renderAction( "saveScene", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )} { "clearCanvas", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )} {this.actionManager.renderAction( "changeViewBackgroundColor", elements, this.state, - this.syncActionResult + this.syncActionResult, + t )} ); @@ -556,6 +578,7 @@ export class App extends React.Component<{}, AppState> { public render() { const canvasWidth = window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT; const canvasHeight = window.innerHeight - CANVAS_WINDOW_OFFSET_TOP; + const { t } = this.props; return (
@@ -624,14 +647,15 @@ export class App extends React.Component<{}, AppState> { ContextMenu.push({ options: [ navigator.clipboard && { - label: "Paste", + label: t("labels.paste"), action: () => this.pasteFromClipboard() }, ...this.actionManager.getContextMenuItems( elements, this.state, this.syncActionResult, - action => this.canvasOnlyActions.includes(action) + action => this.canvasOnlyActions.includes(action), + t ) ], top: e.clientY, @@ -649,18 +673,19 @@ export class App extends React.Component<{}, AppState> { ContextMenu.push({ options: [ navigator.clipboard && { - label: "Copy", + label: t("labels.copy"), action: this.copyToClipboard }, navigator.clipboard && { - label: "Paste", + label: t("labels.paste"), action: () => this.pasteFromClipboard() }, ...this.actionManager.getContextMenuItems( elements, this.state, this.syncActionResult, - action => !this.canvasOnlyActions.includes(action) + action => !this.canvasOnlyActions.includes(action), + t ) ], top: e.clientY, @@ -1333,5 +1358,7 @@ export class App extends React.Component<{}, AppState> { } } +const AppWithTrans = withTranslation()(App); + const rootElement = document.getElementById("root"); -ReactDOM.render(, rootElement); +ReactDOM.render(, rootElement);