Browse Source

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>
Fernando Alava Zambrano 5 years ago
parent
commit
a436e70764

+ 22 - 3
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",

+ 66 - 0
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"
+  }
+}

+ 13 - 13
src/actions/actionProperties.tsx

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

+ 2 - 2
src/components/ExportDialog.tsx

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

+ 32 - 0
src/components/LanguageList.tsx

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

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

+ 11 - 1
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<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>
     );
   }

+ 15 - 6
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();

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