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