diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 30daab83..4743b7bd 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -22,7 +22,20 @@
"withBackground": "With Background",
"handDrawn": "Hand-Drawn",
"normal": "Normal",
- "code": "Code"
+ "code": "Code",
+ "small": "Small",
+ "medium": "Medium",
+ "large": "Large",
+ "veryLarge": "Very Large",
+ "solid": "Solid",
+ "hachure": "Hachure",
+ "crossHatch": "Cross-Hatch",
+ "thin": "Thin",
+ "bold": "Bold",
+ "extraBold": "Extra Bold",
+ "architect": "Architect",
+ "artist": "Artist",
+ "cartoonist": "Cartoonist"
},
"buttons": {
"clearReset": "Clear the canvas & reset background color",
@@ -30,10 +43,16 @@
"exportToPng": "Export to PNG",
"copyToClipboard": "Copy to clipboard",
"save": "Save",
- "load": "Load"
+ "load": "Load",
+ "getShareableLink": "Get shareable link"
},
"alerts": {
- "clearReset": "This will clear the whole canvas. Are you sure?"
+ "clearReset": "This will clear the whole canvas. Are you sure?",
+ "couldNotCreateShareableLink": "Couldn't create shareable link.",
+ "importBackendFailed": "Importing from backend failed.",
+ "cannotExportEmptyCanvas": "Cannot export empty canvas.",
+ "couldNotCopyToClipboard": "Couldn't copy to clipboard. Try using Chrome browser.",
+ "copiedToClipboard": "Copied to clipboard: {{url}}"
},
"toolBar": {
"selection": "Selection",
diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json
new file mode 100644
index 00000000..0e6e0579
--- /dev/null
+++ b/public/locales/es/translation.json
@@ -0,0 +1,66 @@
+{
+ "labels": {
+ "paste": "Pegar",
+ "selectAll": "Seleccionar todo",
+ "copy": "Copiar",
+ "bringForward": "Adelantar",
+ "sendToBack": "Send To Back",
+ "bringToFront": "Traer al frente",
+ "sendBackward": "Enviar átras",
+ "delete": "Borrar",
+ "copyStyles": "Copiar estilos",
+ "pasteStyles": "Pegar estilos",
+ "stroke": "Trazo",
+ "background": "Fondo",
+ "fill": "Rellenar",
+ "strokeWidth": "Ancho de trazo",
+ "sloppiness": "Estilo de trazo",
+ "opacity": "Opacidad",
+ "fontSize": "Tamaño de letra",
+ "fontFamily": "Tipo de letra",
+ "onlySelected": "Sólo seleccionados",
+ "withBackground": "Con fondo",
+ "handDrawn": "Dibujo a Mano",
+ "normal": "Normal",
+ "code": "Código",
+ "small": "Pequeña",
+ "medium": "Mediana",
+ "large": "Grande",
+ "veryLarge": "Muy Grande",
+ "solid": "Sólido",
+ "hachure": "Folleto",
+ "crossHatch": "Rayado transversal",
+ "thin": "Fino",
+ "bold": "Grueso",
+ "extraBold": "Extra Grueso",
+ "architect": "Arquitecto",
+ "artist": "Artista",
+ "cartoonist": "Caricatura"
+ },
+ "buttons": {
+ "clearReset": "Limpiar lienzo y reiniciar el color de fondo",
+ "export": "Exportar",
+ "exportToPng": "Exportar a PNG",
+ "copyToClipboard": "Copiar al portapapeles",
+ "save": "Guardar",
+ "load": "Cargar",
+ "getShareableLink": "Obtener enlace para compartir"
+ },
+ "alerts": {
+ "clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
+ "couldNotCreateShareableLink": "No se pudo crear un enlace para compartir.",
+ "importBackendFailed": "La importación falló.",
+ "cannotExportEmptyCanvas": "No se puede exportar un lienzo vació",
+ "couldNotCopyToClipboard": "No se ha podido copiar al portapapeles, intente usar Chrome como navegador.",
+ "copiedToClipboard": "Copiado en el portapapeles: {{url}}"
+ },
+ "toolBar": {
+ "selection": "Selección",
+ "rectangle": "Rectángulo",
+ "diamond": "Diamante",
+ "ellipse": "Elipse",
+ "arrow": "Flecha",
+ "line": "Línea",
+ "text": "Texto"
+ }
+}
diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx
index ce119d6b..af8f9481 100644
--- a/src/actions/actionProperties.tsx
+++ b/src/actions/actionProperties.tsx
@@ -107,9 +107,9 @@ export const actionChangeFillStyle: Action = {
{t("labels.fill")}
{t("labels.strokeWidth")}
{t("labels.sloppiness")}
{t("labels.fontSize")}
onExportToBackend(exportedElements, 1)}
/>
diff --git a/src/components/LanguageList.tsx b/src/components/LanguageList.tsx
new file mode 100644
index 00000000..e3635b14
--- /dev/null
+++ b/src/components/LanguageList.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+
+export function LanguageList({
+ onClick,
+ languages,
+ currentLanguage
+}: {
+ languages: { lng: string; label: string }[];
+ onClick: (value: string) => void;
+ currentLanguage: string;
+}) {
+ return (
+
+ );
+}
diff --git a/src/i18n.ts b/src/i18n.ts
index 2a6fcdc9..26f9eef0 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -4,18 +4,29 @@ 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: "en", label: "English" },
+ { lng: "es", label: "Español" }
+];
+
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
- backend: {
- loadPath: "./locales/{{lng}}/translation.json"
- },
- lng: "en",
- fallbackLng: "en",
- debug: false,
- react: { useSuspense: false }
+ fallbackLng,
+ react: { useSuspense: false },
+ load: "languageOnly"
});
export default i18n;
diff --git a/src/index.tsx b/src/index.tsx
index dc7b15a3..8020eb5c 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -80,7 +80,8 @@ import { ToolIcon } from "./components/ToolIcon";
import { LockIcon } from "./components/LockIcon";
import { ExportDialog } from "./components/ExportDialog";
import { withTranslation } from "react-i18next";
-import "./i18n";
+import { LanguageList } from "./components/LanguageList";
+import i18n, { languages, parseDetectedLang } from "./i18n";
let { elements } = createScene();
const { history } = createHistory();
@@ -1261,6 +1262,15 @@ export class App extends React.Component {
document.documentElement.style.cursor = hitElement ? "move" : "";
}}
/>
+
+ {
+ i18n.changeLanguage(lng);
+ }}
+ languages={languages}
+ currentLanguage={parseDetectedLang(i18n.language)}
+ />
+
);
}
diff --git a/src/scene/data.ts b/src/scene/data.ts
index 01c636ed..452d3007 100644
--- a/src/scene/data.ts
+++ b/src/scene/data.ts
@@ -8,6 +8,8 @@ import { getExportCanvasPreview } from "./getExportCanvasPreview";
import nanoid from "nanoid";
import { fileOpenPromise, fileSavePromise } from "browser-nativefs";
+import i18n from "../i18n";
+
const LOCAL_STORAGE_KEY = "excalidraw";
const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
const BACKEND_POST = "https://json.excalidraw.com/api/v1/post/";
@@ -120,9 +122,14 @@ export async function exportToBackend(
url.searchParams.append("id", json.id);
await navigator.clipboard.writeText(url.toString());
- window.alert(`Copied to clipboard: ${url.toString()}`);
+ window.alert(
+ i18n.t("alerts.copiedToClipboard", {
+ url: url.toString(),
+ interpolation: { escapeValue: false }
+ })
+ );
} else {
- window.alert("Couldn't create shareable link");
+ window.alert(i18n.t("alerts.couldNotCreateShareableLink"));
}
}
@@ -137,7 +144,7 @@ export async function importFromBackend(id: string | null) {
elements = response.elements || elements;
appState = response.appState || appState;
} catch (error) {
- window.alert("Importing from backend failed");
+ window.alert(i18n.t("alerts.importBackendFailed"));
console.error(error);
}
}
@@ -162,7 +169,8 @@ export async function exportCanvas(
scale?: number;
}
) {
- if (!elements.length) return window.alert("Cannot export empty canvas.");
+ if (!elements.length)
+ return window.alert(i18n.t("alerts.cannotExportEmptyCanvas"));
// calculate smallest area to fit the contents in
const tempCanvas = getExportCanvasPreview(elements, {
@@ -185,6 +193,7 @@ export async function exportCanvas(
}
});
} else if (type === "clipboard") {
+ const errorMsg = i18n.t("alerts.couldNotCopyToClipboard");
try {
tempCanvas.toBlob(async function(blob: any) {
try {
@@ -192,11 +201,11 @@ export async function exportCanvas(
new window.ClipboardItem({ "image/png": blob })
]);
} catch (err) {
- window.alert("Couldn't copy to clipboard. Try using Chrome browser.");
+ window.alert(errorMsg);
}
});
} catch (err) {
- window.alert("Couldn't copy to clipboard. Try using Chrome browser.");
+ window.alert(errorMsg);
}
} else if (type === "backend") {
const appState = getDefaultAppState();
diff --git a/src/styles.scss b/src/styles.scss
index 6fad0242..9bfa90e0 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -183,3 +183,30 @@ button {
}
}
}
+
+.langBox {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ margin-right: 0.5em;
+ ul {
+ margin: 0;
+ padding: 0;
+ }
+ ul > li {
+ list-style: none;
+ display: inline-block;
+ padding: 4px;
+ }
+ li > a,
+ li > a:visited {
+ text-decoration: none;
+ color: gray;
+ font-size: 0.8em;
+ }
+ li.current > a,
+ li.current > a:visited {
+ color: black;
+ text-decoration: underline;
+ }
+}