Internationalization followup (#500)

* add translations in data.ts

* add language list
add spanish version

* fixes pr review

* add more translations

* remove unused label

Co-authored-by: David Luzar <luzar.david@gmail.com>
This commit is contained in:
Fernando Alava Zambrano 2020-01-22 16:25:04 +02:00 committed by Lipis
parent 362cd74a9b
commit a436e70764
9 changed files with 206 additions and 32 deletions

View File

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

View File

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

View File

@ -107,9 +107,9 @@ export const actionChangeFillStyle: Action = {
<h5>{t("labels.fill")}</h5>
<ButtonSelect
options={[
{ value: "solid", text: "Solid" },
{ value: "hachure", text: "Hachure" },
{ value: "cross-hatch", text: "Cross-hatch" }
{ value: "solid", text: t("labels.solid") },
{ value: "hachure", text: t("labels.hachure") },
{ value: "cross-hatch", text: t("labels.crossHatch") }
]}
value={getFormValue(
appState.editingElement,
@ -140,9 +140,9 @@ export const actionChangeStrokeWidth: Action = {
<h5>{t("labels.strokeWidth")}</h5>
<ButtonSelect
options={[
{ value: 1, text: "Thin" },
{ value: 2, text: "Bold" },
{ value: 4, text: "Extra Bold" }
{ value: 1, text: t("labels.thin") },
{ value: 2, text: t("labels.bold") },
{ value: 4, text: t("labels.extraBold") }
]}
value={getFormValue(
appState.editingElement,
@ -171,9 +171,9 @@ export const actionChangeSloppiness: Action = {
<h5>{t("labels.sloppiness")}</h5>
<ButtonSelect
options={[
{ value: 0, text: "Architect" },
{ value: 1, text: "Artist" },
{ value: 3, text: "Cartoonist" }
{ value: 0, text: t("labels.architect") },
{ value: 1, text: t("labels.artist") },
{ value: 3, text: t("labels.cartoonist") }
]}
value={getFormValue(
appState.editingElement,
@ -242,10 +242,10 @@ export const actionChangeFontSize: Action = {
<h5>{t("labels.fontSize")}</h5>
<ButtonSelect
options={[
{ value: 16, text: "Small" },
{ value: 20, text: "Medium" },
{ value: 28, text: "Large" },
{ value: 36, text: "Very Large" }
{ value: 16, text: t("labels.small") },
{ value: 20, text: t("labels.medium") },
{ value: 28, text: t("labels.large") },
{ value: 36, text: t("labels.veryLarge") }
]}
value={getFormValue(
appState.editingElement,

View File

@ -127,8 +127,8 @@ export function ExportDialog({
<ToolIcon
type="button"
icon={link}
title="Get shareable link"
aria-label="Get shareable link"
title={t("buttons.getShareableLink")}
aria-label={t("buttons.getShareableLink")}
onClick={() => onExportToBackend(exportedElements, 1)}
/>
</Stack.Row>

View File

@ -0,0 +1,32 @@
import React from "react";
export function LanguageList<T>({
onClick,
languages,
currentLanguage
}: {
languages: { lng: string; label: string }[];
onClick: (value: string) => void;
currentLanguage: string;
}) {
return (
<ul>
{languages.map((language, idx) => (
<li
key={idx}
className={currentLanguage === language.lng ? "current" : ""}
>
<a
href="/"
onClick={e => {
onClick(language.lng);
e.preventDefault();
}}
>
{language.label}
</a>
</li>
))}
</ul>
);
}

View File

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

View File

@ -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<any, AppState> {
document.documentElement.style.cursor = hitElement ? "move" : "";
}}
/>
<div className="langBox">
<LanguageList
onClick={lng => {
i18n.changeLanguage(lng);
}}
languages={languages}
currentLanguage={parseDetectedLang(i18n.language)}
/>
</div>
</div>
);
}

View File

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

View File

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