mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-02 03:25:53 +01:00
Internationalization support (#477)
* add i18next lib add some translations * add translations * fix font-family * fix pin versions
This commit is contained in:
parent
1a03a29025
commit
ff7a340d2f
128
package-lock.json
generated
128
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
47
public/locales/en/translation.json
Normal file
47
public/locales/en/translation.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
@ -31,14 +31,14 @@ export const actionClearCanvas: Action = {
|
||||
appState: getDefaultAppState()
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ updateData }) => (
|
||||
PanelComponent: ({ updateData, t }) => (
|
||||
<ToolIcon
|
||||
type="button"
|
||||
icon={trash}
|
||||
title="Clear the canvas & reset background color"
|
||||
aria-label="Clear the canvas & reset background color"
|
||||
title={t("buttons.clearReset")}
|
||||
aria-label={t("buttons.clearReset")}
|
||||
onClick={() => {
|
||||
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`.
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ export const actionChangeExportBackground: Action = {
|
||||
perform: (elements, appState, value) => {
|
||||
return { appState: { ...appState, exportBackground: value } };
|
||||
},
|
||||
PanelComponent: ({ appState, updateData }) => (
|
||||
PanelComponent: ({ appState, updateData, t }) => (
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -32,7 +32,7 @@ export const actionChangeExportBackground: Action = {
|
||||
updateData(e.target.checked);
|
||||
}}
|
||||
/>{" "}
|
||||
With background
|
||||
{t("labels.withBackground")}
|
||||
</label>
|
||||
)
|
||||
};
|
||||
@ -43,12 +43,12 @@ export const actionSaveScene: Action = {
|
||||
saveAsJSON(elements, appState).catch(err => console.error(err));
|
||||
return {};
|
||||
},
|
||||
PanelComponent: ({ updateData }) => (
|
||||
PanelComponent: ({ updateData, t }) => (
|
||||
<ToolIcon
|
||||
type="button"
|
||||
icon={save}
|
||||
title="Save"
|
||||
aria-label="Save"
|
||||
title={t("buttons.save")}
|
||||
aria-label={t("buttons.save")}
|
||||
onClick={() => updateData(null)}
|
||||
/>
|
||||
)
|
||||
@ -63,12 +63,12 @@ export const actionLoadScene: Action = {
|
||||
) => {
|
||||
return { elements: loadedElements, appState: loadedAppState };
|
||||
},
|
||||
PanelComponent: ({ updateData }) => (
|
||||
PanelComponent: ({ updateData, t }) => (
|
||||
<ToolIcon
|
||||
type="button"
|
||||
icon={load}
|
||||
title="Load"
|
||||
aria-label="Load"
|
||||
title={t("buttons.load")}
|
||||
aria-label={t("buttons.load")}
|
||||
onClick={() => {
|
||||
loadFromJSON()
|
||||
.then(({ elements, appState }) => {
|
||||
|
@ -30,19 +30,21 @@ export const actionChangeStrokeColor: Action = {
|
||||
appState: { ...appState, currentItemStrokeColor: value }
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<>
|
||||
<h5>Stroke</h5>
|
||||
<ColorPicker
|
||||
type="elementStroke"
|
||||
color={
|
||||
getSelectedAttribute(elements, element => element.strokeColor) ||
|
||||
appState.currentItemStrokeColor
|
||||
}
|
||||
onChange={updateData}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
PanelComponent: ({ elements, appState, updateData, t }) => {
|
||||
return (
|
||||
<>
|
||||
<h5>{t("labels.stroke")}</h5>
|
||||
<ColorPicker
|
||||
type="elementStroke"
|
||||
color={
|
||||
getSelectedAttribute(elements, element => 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 }) => (
|
||||
<>
|
||||
<h5>Background</h5>
|
||||
<h5>{t("labels.background")}</h5>
|
||||
<ColorPicker
|
||||
type="elementBackground"
|
||||
color={
|
||||
@ -83,9 +85,9 @@ export const actionChangeFillStyle: Action = {
|
||||
}))
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, updateData }) => (
|
||||
PanelComponent: ({ elements, updateData, t }) => (
|
||||
<>
|
||||
<h5>Fill</h5>
|
||||
<h5>{t("labels.fill")}</h5>
|
||||
<ButtonSelect
|
||||
options={[
|
||||
{ value: "solid", text: "Solid" },
|
||||
@ -112,9 +114,9 @@ export const actionChangeStrokeWidth: Action = {
|
||||
}))
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
PanelComponent: ({ elements, appState, updateData, t }) => (
|
||||
<>
|
||||
<h5>Stroke Width</h5>
|
||||
<h5>{t("labels.strokeWidth")}</h5>
|
||||
<ButtonSelect
|
||||
options={[
|
||||
{ value: 1, text: "Thin" },
|
||||
@ -139,9 +141,9 @@ export const actionChangeSloppiness: Action = {
|
||||
}))
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
PanelComponent: ({ elements, appState, updateData, t }) => (
|
||||
<>
|
||||
<h5>Sloppiness</h5>
|
||||
<h5>{t("labels.sloppiness")}</h5>
|
||||
<ButtonSelect
|
||||
options={[
|
||||
{ value: 0, text: "Architect" },
|
||||
@ -166,9 +168,9 @@ export const actionChangeOpacity: Action = {
|
||||
}))
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, updateData }) => (
|
||||
PanelComponent: ({ elements, updateData, t }) => (
|
||||
<>
|
||||
<h5>Opacity</h5>
|
||||
<h5>{t("labels.oppacity")}</h5>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
@ -202,9 +204,9 @@ export const actionChangeFontSize: Action = {
|
||||
})
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, updateData }) => (
|
||||
PanelComponent: ({ elements, updateData, t }) => (
|
||||
<>
|
||||
<h5>Font size</h5>
|
||||
<h5>{t("labels.fontSize")}</h5>
|
||||
<ButtonSelect
|
||||
options={[
|
||||
{ value: 16, text: "Small" },
|
||||
@ -241,14 +243,14 @@ export const actionChangeFontFamily: Action = {
|
||||
})
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, updateData }) => (
|
||||
PanelComponent: ({ elements, updateData, t }) => (
|
||||
<>
|
||||
<h5>Font family</h5>
|
||||
<h5>{t("labels.fontFamily")}</h5>
|
||||
<ButtonSelect
|
||||
options={[
|
||||
{ value: "Virgil", text: "Hand-drawn" },
|
||||
{ value: "Helvetica", text: "Normal" },
|
||||
{ value: "Cascadia", text: "Code" }
|
||||
{ value: "Virgil", text: t("labels.handDrawn") },
|
||||
{ value: "Helvetica", text: t("labels.normal") },
|
||||
{ value: "Cascadia", text: t("labels.code") }
|
||||
]}
|
||||
value={getSelectedAttribute(
|
||||
elements,
|
||||
|
@ -8,6 +8,6 @@ export const actionSelectAll: Action = {
|
||||
elements: elements.map(elem => ({ ...elem, isSelected: true }))
|
||||
};
|
||||
},
|
||||
contextItemLabel: "Select All",
|
||||
contextItemLabel: "labels.selectAll",
|
||||
keyTest: event => event[KEYS.META] && event.code === "KeyA"
|
||||
};
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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"
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 && (
|
||||
<Modal maxWidth={640} onCloseRequest={handleClose}>
|
||||
@ -99,23 +102,23 @@ export function ExportDialog({
|
||||
<button className="ExportDialog__close" onClick={handleClose}>
|
||||
╳
|
||||
</button>
|
||||
<h2>Export</h2>
|
||||
<h2>{t("buttons.export")}</h2>
|
||||
<div className="ExportDialog__preview" ref={previeRef}></div>
|
||||
<div className="ExportDialog__actions">
|
||||
<Stack.Row gap={2}>
|
||||
<ToolIcon
|
||||
type="button"
|
||||
icon={downloadFile}
|
||||
title="Export to PNG"
|
||||
aria-label="Export to PNG"
|
||||
title={t("buttons.exportToPng")}
|
||||
aria-label={t("buttons.exportToPng")}
|
||||
onClick={() => onExportToPng(exportedElements, scale)}
|
||||
/>
|
||||
{probablySupportsClipboard && (
|
||||
<ToolIcon
|
||||
type="button"
|
||||
icon={clipboard}
|
||||
title="Copy to clipboard"
|
||||
aria-label="Copy to clipboard"
|
||||
title={t("buttons.copyToClipboard")}
|
||||
aria-label={t("buttons.copyToClipboard")}
|
||||
onClick={() =>
|
||||
onExportToClipboard(exportedElements, scale)
|
||||
}
|
||||
@ -134,7 +137,8 @@ export function ExportDialog({
|
||||
"changeProjectName",
|
||||
elements,
|
||||
appState,
|
||||
syncActionResult
|
||||
syncActionResult,
|
||||
t
|
||||
)}
|
||||
<Stack.Col gap={1}>
|
||||
<div className="ExportDialog__scales">
|
||||
@ -157,7 +161,8 @@ export function ExportDialog({
|
||||
"changeExportBackground",
|
||||
elements,
|
||||
appState,
|
||||
syncActionResult
|
||||
syncActionResult,
|
||||
t
|
||||
)}
|
||||
{someElementIsSelected && (
|
||||
<div>
|
||||
@ -169,7 +174,7 @@ export function ExportDialog({
|
||||
setExportSelected(e.currentTarget.checked)
|
||||
}
|
||||
/>{" "}
|
||||
Only selected
|
||||
{t("labels.onlySelected")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
21
src/i18n.ts
Normal file
21
src/i18n.ts
Normal file
@ -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;
|
105
src/index.tsx
105
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<any, AppState> {
|
||||
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
|
||||
)}
|
||||
<hr />
|
||||
</>
|
||||
@ -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
|
||||
)}
|
||||
<hr />
|
||||
</>
|
||||
@ -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
|
||||
)}
|
||||
<hr />
|
||||
</>
|
||||
@ -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
|
||||
)}
|
||||
</div>
|
||||
</Island>
|
||||
@ -462,32 +474,38 @@ export class App extends React.Component<{}, AppState> {
|
||||
}
|
||||
|
||||
private renderShapesSwitcher() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{SHAPES.map(({ value, icon }, index) => (
|
||||
<ToolIcon
|
||||
key={value}
|
||||
type="radio"
|
||||
icon={icon}
|
||||
checked={this.state.elementType === value}
|
||||
name="editor-current-shape"
|
||||
title={`${capitalizeString(value)} — ${
|
||||
capitalizeString(value)[0]
|
||||
}, ${index + 1}`}
|
||||
onChange={() => {
|
||||
this.setState({ elementType: value });
|
||||
elements = clearSelection(elements);
|
||||
document.documentElement.style.cursor =
|
||||
value === "text" ? "text" : "crosshair";
|
||||
this.forceUpdate();
|
||||
}}
|
||||
></ToolIcon>
|
||||
))}
|
||||
{SHAPES.map(({ value, icon }, index) => {
|
||||
const label = t(`toolBar.${value}`);
|
||||
return (
|
||||
<ToolIcon
|
||||
key={value}
|
||||
type="radio"
|
||||
icon={icon}
|
||||
checked={this.state.elementType === value}
|
||||
name="editor-current-shape"
|
||||
title={`${capitalizeString(label)} — ${
|
||||
capitalizeString(label)[0]
|
||||
}, ${index + 1}`}
|
||||
onChange={() => {
|
||||
this.setState({ elementType: value });
|
||||
elements = clearSelection(elements);
|
||||
document.documentElement.style.cursor =
|
||||
value === "text" ? "text" : "crosshair";
|
||||
this.forceUpdate();
|
||||
}}
|
||||
></ToolIcon>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderCanvasActions() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Stack.Col gap={4}>
|
||||
<Stack.Row justifyContent={"space-between"}>
|
||||
@ -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
|
||||
)}
|
||||
<ExportDialog
|
||||
elements={elements}
|
||||
@ -540,14 +560,16 @@ export class App extends React.Component<{}, AppState> {
|
||||
"clearCanvas",
|
||||
elements,
|
||||
this.state,
|
||||
this.syncActionResult
|
||||
this.syncActionResult,
|
||||
t
|
||||
)}
|
||||
</Stack.Row>
|
||||
{this.actionManager.renderAction(
|
||||
"changeViewBackgroundColor",
|
||||
elements,
|
||||
this.state,
|
||||
this.syncActionResult
|
||||
this.syncActionResult,
|
||||
t
|
||||
)}
|
||||
</Stack.Col>
|
||||
);
|
||||
@ -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 (
|
||||
<div className="container">
|
||||
@ -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(<App />, rootElement);
|
||||
ReactDOM.render(<AppWithTrans />, rootElement);
|
||||
|
Loading…
Reference in New Issue
Block a user