Replace i18n by a custom implementation (#638)

There are two problems with the current localization strategy:
- We download the translations on-demand, which means that it does a serial roundtrip for nothing.
- withTranslation helper actually renders the app 3 times on startup, instead of once (I haven't tried to debug it)
This commit is contained in:
Christopher Chedeau 2020-01-31 21:06:06 +00:00 committed by GitHub
parent 637276301a
commit e4919e2e6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 101 additions and 167 deletions

View file

@ -4,13 +4,14 @@ import { ColorPicker } from "../components/ColorPicker";
import { getDefaultAppState } from "../appState";
import { trash } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
export const actionChangeViewBackgroundColor: Action = {
name: "changeViewBackgroundColor",
perform: (elements, appState, value) => {
return { appState: { ...appState, viewBackgroundColor: value } };
},
PanelComponent: ({ appState, updateData, t }) => {
PanelComponent: ({ appState, updateData }) => {
return (
<div style={{ position: "relative" }}>
<ColorPicker
@ -32,7 +33,7 @@ export const actionClearCanvas: Action = {
appState: getDefaultAppState(),
};
},
PanelComponent: ({ updateData, t }) => (
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
icon={trash}

View file

@ -4,13 +4,14 @@ import { ProjectName } from "../components/ProjectName";
import { saveAsJSON, loadFromJSON } from "../scene";
import { load, save } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
export const actionChangeProjectName: Action = {
name: "changeProjectName",
perform: (elements, appState, value) => {
return { appState: { ...appState, name: value } };
},
PanelComponent: ({ appState, updateData, t }) => (
PanelComponent: ({ appState, updateData }) => (
<ProjectName
label={t("labels.fileTitle")}
value={appState.name || "Unnamed"}
@ -24,7 +25,7 @@ export const actionChangeExportBackground: Action = {
perform: (elements, appState, value) => {
return { appState: { ...appState, exportBackground: value } };
},
PanelComponent: ({ appState, updateData, t }) => (
PanelComponent: ({ appState, updateData }) => (
<label>
<input
type="checkbox"
@ -44,7 +45,7 @@ export const actionSaveScene: Action = {
saveAsJSON(elements, appState).catch(err => console.error(err));
return {};
},
PanelComponent: ({ updateData, t }) => (
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
icon={save}
@ -64,7 +65,7 @@ export const actionLoadScene: Action = {
) => {
return { elements: loadedElements, appState: loadedAppState };
},
PanelComponent: ({ updateData, t }) => (
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
icon={load}

View file

@ -6,6 +6,7 @@ import { ButtonSelect } from "../components/ButtonSelect";
import { isTextElement, redrawTextBoundingBox } from "../element";
import { ColorPicker } from "../components/ColorPicker";
import { AppState } from "../../src/types";
import { t } from "../i18n";
const changeProperty = (
elements: readonly ExcalidrawElement[],
@ -46,7 +47,7 @@ export const actionChangeStrokeColor: Action = {
appState: { ...appState, currentItemStrokeColor: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<>
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
<ColorPicker
@ -76,7 +77,7 @@ export const actionChangeBackgroundColor: Action = {
appState: { ...appState, currentItemBackgroundColor: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<>
<h3 aria-hidden="true">{t("labels.background")}</h3>
<ColorPicker
@ -106,7 +107,7 @@ export const actionChangeFillStyle: Action = {
appState: { ...appState, currentItemFillStyle: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.fill")}</legend>
<ButtonSelect
@ -142,7 +143,7 @@ export const actionChangeStrokeWidth: Action = {
appState: { ...appState, currentItemStrokeWidth: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.strokeWidth")}</legend>
<ButtonSelect
@ -176,7 +177,7 @@ export const actionChangeSloppiness: Action = {
appState: { ...appState, currentItemRoughness: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.sloppiness")}</legend>
<ButtonSelect
@ -210,7 +211,7 @@ export const actionChangeOpacity: Action = {
appState: { ...appState, currentItemOpacity: value },
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<label className="control-label">
{t("labels.opacity")}
<input
@ -256,7 +257,7 @@ export const actionChangeFontSize: Action = {
},
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.fontSize")}</legend>
<ButtonSelect
@ -304,7 +305,7 @@ export const actionChangeFontFamily: Action = {
},
};
},
PanelComponent: ({ elements, appState, updateData, t }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.fontFamily")}</legend>
<ButtonSelect

View file

@ -7,7 +7,7 @@ import {
} from "./types";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { TFunction } from "i18next";
import { t } from "../i18n";
export class ActionManager implements ActionsManagerInterface {
actions: { [keyProp: string]: Action } = {};
@ -48,7 +48,6 @@ export class ActionManager implements ActionsManagerInterface {
appState: AppState,
updater: UpdaterFn,
actionFilter: ActionFilterFn = action => action,
t?: TFunction,
) {
return Object.values(this.actions)
.filter(actionFilter)
@ -59,10 +58,7 @@ export class ActionManager implements ActionsManagerInterface {
(b.contextMenuOrder !== undefined ? b.contextMenuOrder : 999),
)
.map(action => ({
label:
t && action.contextItemLabel
? t(action.contextItemLabel)
: action.contextItemLabel!,
label: action.contextItemLabel ? t(action.contextItemLabel) : "",
action: () => {
updater(action.perform(elements, appState, null));
},
@ -74,7 +70,6 @@ export class ActionManager implements ActionsManagerInterface {
elements: readonly ExcalidrawElement[],
appState: AppState,
updater: UpdaterFn,
t: TFunction,
) {
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
const action = this.actions[name];
@ -88,7 +83,6 @@ export class ActionManager implements ActionsManagerInterface {
elements={elements}
appState={appState}
updateData={updateData}
t={t}
/>
);
}

View file

@ -1,7 +1,6 @@
import React from "react";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { TFunction } from "i18next";
export type ActionResult = {
elements?: ExcalidrawElement[];
@ -23,7 +22,6 @@ export interface Action {
elements: readonly ExcalidrawElement[];
appState: AppState;
updateData: (formData: any) => void;
t: TFunction;
}>;
perform: ActionFn;
keyPriority?: number;
@ -57,6 +55,5 @@ export interface ActionsManagerInterface {
elements: readonly ExcalidrawElement[],
appState: AppState,
updater: UpdaterFn,
t: TFunction,
) => React.ReactElement | null;
}

View file

@ -3,8 +3,7 @@ import { Popover } from "./Popover";
import "./ColorPicker.css";
import { KEYS } from "../keys";
import { useTranslation } from "react-i18next";
import { TFunction } from "i18next";
import { t } from "../i18n";
// This is a narrow reimplementation of the awesome react-color Twitter component
// https://github.com/casesandberg/react-color/blob/master/src/components/twitter/Twitter.js
@ -15,14 +14,12 @@ const Picker = function({
onChange,
onClose,
label,
t,
}: {
colors: string[];
color: string | null;
onChange: (color: string) => void;
onClose: () => void;
label: string;
t: TFunction;
}) {
const firstItem = React.useRef<HTMLButtonElement>();
const colorInput = React.useRef<HTMLInputElement>();
@ -158,8 +155,6 @@ export function ColorPicker({
onChange: (color: string) => void;
label: string;
}) {
const { t } = useTranslation();
const [isActive, setActive] = React.useState(false);
const pickerButton = React.useRef<HTMLButtonElement>(null);
@ -195,7 +190,6 @@ export function ColorPicker({
pickerButton.current?.focus();
}}
label={label}
t={t}
/>
</Popover>
) : null}

View file

@ -11,8 +11,8 @@ import { AppState } from "../types";
import { exportToCanvas } from "../scene/export";
import { ActionsManagerInterface, UpdaterFn } from "../actions/types";
import Stack from "./Stack";
import { t } from "../i18n";
import { useTranslation } from "react-i18next";
import { KEYS } from "../keys";
const probablySupportsClipboard =
@ -52,7 +52,6 @@ function ExportModal({
onExportToBackend: ExportCB;
onCloseRequest: () => void;
}) {
const { t } = useTranslation();
const someElementIsSelected = elements.some(element => element.isSelected);
const [scale, setScale] = useState(defaultScale);
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
@ -170,7 +169,6 @@ function ExportModal({
elements,
appState,
syncActionResult,
t,
)}
<Stack.Col gap={1}>
<div className="ExportDialog__scales">
@ -195,7 +193,6 @@ function ExportModal({
elements,
appState,
syncActionResult,
t,
)}
{someElementIsSelected && (
<div>
@ -238,7 +235,6 @@ export function ExportDialog({
onExportToClipboard: ExportCB;
onExportToBackend: ExportCB;
}) {
const { t } = useTranslation();
const [modalIsShown, setModalIsShown] = useState(false);
const triggerButton = useRef<HTMLButtonElement>(null);

View file

@ -1,22 +1,20 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { t } from "../i18n";
export function LanguageList<T>({
onClick,
onChange,
languages,
currentLanguage,
}: {
languages: { lng: string; label: string }[];
onClick: (value: string) => void;
onChange: (value: string) => void;
currentLanguage: string;
}) {
const { t } = useTranslation();
return (
<React.Fragment>
<select
className="language-select"
onChange={({ target }) => onClick(target.value)}
onChange={({ target }) => onChange(target.value)}
value={currentLanguage}
aria-label={t("buttons.selectLanguage")}
>

View file

@ -6,10 +6,6 @@ import { PreviousScene } from "../scene/types";
Enzyme.configure({ adapter: new Adapter() });
jest.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: any) => key }),
}));
function setup(props: any) {
const currentProps = {
...props,

View file

@ -1,6 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { PreviousScene } from "../scene/types";
import { t } from "../i18n";
interface StoredScenesListProps {
scenes: PreviousScene[];
@ -13,8 +13,6 @@ export function StoredScenesList({
currentId,
onChange,
}: StoredScenesListProps) {
const { t } = useTranslation();
return (
<React.Fragment>
<select

View file

@ -1,36 +1,68 @@
import i18n from "i18next";
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: "de", label: "Deutsch" },
{ lng: "en", label: "English" },
{ lng: "es", label: "Español" },
{ lng: "fr", label: "Français" },
{ lng: "pt", label: "Português" },
{ lng: "ru", label: "Русский" },
{ lng: "en", label: "English", data: require("./locales/en.json") },
{ lng: "de", label: "Deutsch", data: require("./locales/de.json") },
{ lng: "es", label: "Español", data: require("./locales/es.json") },
{ lng: "fr", label: "Français", data: require("./locales/fr.json") },
{ lng: "pt", label: "Português", data: require("./locales/pt.json") },
{ lng: "ru", label: "Русский", data: require("./locales/ru.json") },
];
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng,
react: { useSuspense: false },
load: "languageOnly",
});
let currentLanguage = languages[0];
const fallbackLanguage = languages[0];
export default i18n;
export function setLanguage(newLng: string | undefined) {
currentLanguage =
languages.find(language => language.lng === newLng) || fallbackLanguage;
languageDetector.cacheUserLanguage(currentLanguage.lng);
}
export function getLanguage() {
return currentLanguage.lng;
}
function findPartsForData(data: any, parts: string[]) {
for (var i = 0; i < parts.length; ++i) {
const part = parts[i];
if (data[part] === undefined) {
return undefined;
}
data = data[part];
}
if (typeof data !== "string") {
return undefined;
}
return data;
}
export function t(path: string, replacement?: { [key: string]: string }) {
const parts = path.split(".");
let translation =
findPartsForData(currentLanguage.data, parts) ||
findPartsForData(fallbackLanguage.data, parts);
if (translation === undefined) {
throw new Error("Can't find translation for " + path);
}
if (replacement) {
for (var key in replacement) {
translation = translation.replace("{{" + key + "}}", replacement[key]);
}
}
return translation;
}
const languageDetector = new LanguageDetector();
languageDetector.init({
languageUtils: {
formatLanguageCode: function(lng: string) {
return lng;
},
isWhitelisted: () => true,
},
checkWhitelist: false,
});
setLanguage(languageDetector.detect());

View file

@ -85,9 +85,8 @@ import { FixedSideContainer } from "./components/FixedSideContainer";
import { ToolButton } from "./components/ToolButton";
import { LockIcon } from "./components/LockIcon";
import { ExportDialog } from "./components/ExportDialog";
import { withTranslation } from "react-i18next";
import { LanguageList } from "./components/LanguageList";
import i18n, { languages, parseDetectedLang } from "./i18n";
import { t, languages, setLanguage, getLanguage } from "./i18n";
import { StoredScenesList } from "./components/StoredScenesList";
let { elements } = createScene();
@ -448,7 +447,6 @@ export class App extends React.Component<any, AppState> {
};
private renderSelectedShapeActions(elements: readonly ExcalidrawElement[]) {
const { t } = this.props;
const { elementType, editingElement } = this.state;
const targetElements = editingElement
? [editingElement]
@ -465,7 +463,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{(hasBackground(elementType) ||
targetElements.some(element => hasBackground(element.type))) && (
@ -475,7 +472,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{this.actionManager.renderAction(
@ -483,7 +479,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</>
)}
@ -496,7 +491,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{this.actionManager.renderAction(
@ -504,7 +498,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</>
)}
@ -517,7 +510,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{this.actionManager.renderAction(
@ -525,7 +517,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</>
)}
@ -535,7 +526,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{this.actionManager.renderAction(
@ -543,7 +533,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</div>
</Island>
@ -551,8 +540,6 @@ export class App extends React.Component<any, AppState> {
}
private renderShapesSwitcher() {
const { t } = this.props;
return (
<>
{SHAPES.map(({ value, icon }, index) => {
@ -584,7 +571,6 @@ export class App extends React.Component<any, AppState> {
}
private renderCanvasActions() {
const { t } = this.props;
return (
<Stack.Col gap={4}>
<Stack.Row justifyContent={"space-between"}>
@ -593,14 +579,12 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
{this.actionManager.renderAction(
"saveScene",
elements,
this.state,
this.syncActionResult,
t,
)}
<ExportDialog
elements={elements}
@ -653,7 +637,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</Stack.Row>
{this.actionManager.renderAction(
@ -661,7 +644,6 @@ export class App extends React.Component<any, AppState> {
elements,
this.state,
this.syncActionResult,
t,
)}
</Stack.Col>
);
@ -670,7 +652,6 @@ export class App extends React.Component<any, 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">
@ -779,7 +760,6 @@ export class App extends React.Component<any, AppState> {
this.state,
this.syncActionResult,
action => this.canvasOnlyActions.includes(action),
t,
),
],
top: e.clientY,
@ -809,7 +789,6 @@ export class App extends React.Component<any, AppState> {
this.state,
this.syncActionResult,
action => !this.canvasOnlyActions.includes(action),
t,
),
],
top: e.clientY,
@ -1480,11 +1459,12 @@ export class App extends React.Component<any, AppState> {
</main>
<footer role="contentinfo">
<LanguageList
onClick={lng => {
i18n.changeLanguage(lng);
onChange={lng => {
setLanguage(lng);
this.setState({});
}}
languages={languages}
currentLanguage={parseDetectedLang(i18n.language)}
currentLanguage={getLanguage()}
/>
{this.renderIdsDropdown()}
</footer>
@ -1614,8 +1594,6 @@ export class App extends React.Component<any, AppState> {
}
}
const AppWithTrans = withTranslation()(App);
const rootElement = document.getElementById("root");
class TopErrorBoundary extends React.Component {
@ -1710,7 +1688,7 @@ class TopErrorBoundary extends React.Component {
ReactDOM.render(
<TopErrorBoundary>
<AppWithTrans />
<App />
</TopErrorBoundary>,
rootElement,
);

67
src/locales/de.json Normal file
View file

@ -0,0 +1,67 @@
{
"alerts": {
"cannotExportEmptyCanvas": "Leere Zeichenfläche kann nicht exportiert werden.",
"clearReset": "Dies wird die ganze Zeichenfläche löschen. Bist du dir sicher?",
"copiedToClipboard": "In Zwischenablage kopiert: {{url}}",
"couldNotCopyToClipboard": "Konnte nicht in die Zwischenablage kopieren. Versuch es mit dem Chrome Browser.",
"couldNotCreateShareableLink": "Konnte keinen teilbaren Link erstellen.",
"importBackendFailed": "Import vom Server ist fehlgeschlagen."
},
"buttons": {
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
"copyToClipboard": "In die Zwischenablage kopieren",
"export": "Export",
"exportToPng": "Als PNG exportieren",
"exportToSvg": "Als SVG exportieren",
"getShareableLink": "Teilbaren Link erhalten",
"load": "Laden",
"save": "Speichern"
},
"labels": {
"architect": "Architekt",
"artist": "Künstler",
"background": "Hintergrund",
"bold": "Fett",
"bringForward": "Nach vorne",
"bringToFront": "In den Vordergrund",
"cartoonist": "Karikaturist",
"code": "Code",
"copy": "Kopieren",
"copyStyles": "Stile kopieren",
"crossHatch": "Kreuzschraffiert",
"delete": "Löschen",
"extraBold": "Extra Fett",
"fill": "Füllung",
"fontFamily": "Schriftart",
"fontSize": "Schriftgröße",
"hachure": "Schraffiert",
"handDrawn": "Handschrift",
"large": "Groß",
"medium": "Mittel",
"normal": "Normal",
"onlySelected": "Nur ausgewählte",
"opacity": "Sichtbarkeit",
"paste": "Einfügen",
"pasteStyles": "Stile einfügen",
"selectAll": "Alle auswählen",
"sendBackward": "Nach hinten",
"sendToBack": "In den Hintergrund",
"sloppiness": "Sauberkeit",
"small": "Klein",
"solid": "Solide",
"stroke": "Strich",
"strokeWidth": "Strichstärke",
"thin": "Dünn",
"veryLarge": "Sehr Groß",
"withBackground": "Mit Hintergrund"
},
"toolBar": {
"arrow": "Pfeil",
"diamond": "Raute",
"ellipse": "Ellipse",
"line": "Linie",
"rectangle": "Rechteck",
"selection": "Auswahl",
"text": "Text"
}
}

80
src/locales/en.json Normal file
View file

@ -0,0 +1,80 @@
{
"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",
"opacity": "Opacity",
"fontSize": "Font Size",
"fontFamily": "Font Family",
"onlySelected": "Only selected",
"withBackground": "With Background",
"handDrawn": "Hand-Drawn",
"normal": "Normal",
"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",
"fileTitle": "File title",
"colorPicker": "Color picker",
"canvasBackground": "Canvas background",
"drawingCanvas": "Drawing Canvas"
},
"buttons": {
"clearReset": "Clear the canvas & reset background color",
"export": "Export",
"exportToPng": "Export to PNG",
"exportToSvg": "Export to SVG",
"copyToClipboard": "Copy to clipboard",
"save": "Save",
"load": "Load",
"getShareableLink": "Get shareable link",
"close": "Close",
"selectLanguage": "Select Language",
"previouslyLoadedScenes": "Previously loaded scenes"
},
"alerts": {
"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",
"rectangle": "Rectangle",
"diamond": "Diamond",
"ellipse": "Ellipse",
"arrow": "Arrow",
"line": "Line",
"text": "Text",
"lock": "Keep selected tool active after drawing"
},
"headings": {
"canvasActions": "Canvas actions",
"selectedShapeActions": "Selected shape actions",
"shapes": "Shapes"
}
}

81
src/locales/es.json Normal file
View file

@ -0,0 +1,81 @@
{
"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",
"fileTitle": "Título del archivo",
"colorPicker": "Selector de color",
"canvasBackground": "Fondo del lienzo",
"drawingCanvas": "Lienzo de dibujo"
},
"buttons": {
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
"export": "Exportar",
"exportToPng": "Exportar a PNG",
"exportToSvg": "Exportar a SVG",
"copyToClipboard": "Copiar al portapapeles",
"save": "Guardar",
"load": "Cargar",
"getShareableLink": "Obtener enlace para compartir",
"showExportDialog": "Mostrar diálogo para exportar",
"close": "Cerrar",
"selectLanguage": "Seleccionar idioma",
"previouslyLoadedScenes": "Escenas previamente cargadas"
},
"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",
"lock": "Mantener la herramienta seleccionada activa después de dibujar"
},
"headings": {
"canvasActions": "Acciones del lienzo",
"selectedShapeActions": "Acciones de la forma seleccionada",
"shapes": "Formas"
}
}

68
src/locales/fr.json Normal file
View file

@ -0,0 +1,68 @@
{
"labels": {
"paste": "Coller",
"selectAll": "Tout sélectionner",
"copy": "Copier",
"bringForward": "Mettre en avant",
"sendToBack": "Mettre en arrière-plan",
"bringToFront": "Mettre au premier plan",
"sendBackward": "Mettre en arrière",
"delete": "Supprimer",
"copyStyles": "Copier les styles",
"pasteStyles": "Coller les styles",
"stroke": "Contour",
"background": "Arrière-plan",
"fill": "Remplissage",
"strokeWidth": "Épaisseur contour",
"sloppiness": "Laisser-aller",
"opacity": "Opacité",
"fontSize": "Taille police",
"fontFamily": "Police",
"onlySelected": "Uniquement la sélection",
"withBackground": "Avec arrière-plan",
"handDrawn": "Manuscrite",
"normal": "Normale",
"code": "Code",
"small": "Petit",
"medium": "Moyen",
"large": "Large",
"veryLarge": "Très Large",
"solid": "Solide",
"hachure": "Hachure",
"crossHatch": "Hachure croisée",
"thin": "Fin",
"bold": "Épais",
"extraBold": "Très épais",
"architect": "Architecte",
"artist": "Artiste",
"cartoonist": "Cartooniste"
},
"buttons": {
"clearReset": "Effacer le canvas & réinitialiser la couleur d'arrière-plan",
"export": "Exporter",
"exportToPng": "Exporter en PNG",
"exportToSvg": "Exporter en SVG",
"copyToClipboard": "Copier dans le presse-papier",
"save": "Sauvegarder",
"load": "Ouvrir",
"getShareableLink": "Obtenir un lien de partage",
"previouslyLoadedScenes": "Scènes précédemment chargées"
},
"alerts": {
"clearReset": "L'intégralité du canvas va être effacé. Êtes-vous sur ?",
"couldNotCreateShareableLink": "Impossible de créer un lien de partage.",
"importBackendFailed": "L'import depuis le backend a échoué.",
"cannotExportEmptyCanvas": "Impossible d'exporter un canvas vide.",
"couldNotCopyToClipboard": "Impossible de copier dans le presse-papier. Essayez d'utiliser le navigateur Chrome.",
"copiedToClipboard": "Copié dans le presse-papier: {{url}}"
},
"toolBar": {
"selection": "Sélection",
"rectangle": "Rectangle",
"diamond": "Losange",
"ellipse": "Ellipse",
"arrow": "Flèche",
"line": "Ligne",
"text": "Texte"
}
}

68
src/locales/pt.json Normal file
View file

@ -0,0 +1,68 @@
{
"labels": {
"paste": "Colar",
"selectAll": "Selecionar tudo",
"copy": "Copiar",
"bringForward": "Passar para o primeiro plano",
"sendToBack": "Passar para trás",
"bringToFront": "Passar para frente",
"sendBackward": "Passar para o plano de fundo",
"delete": "Apagar",
"copyStyles": "Copiar os estilos",
"pasteStyles": "Colar os estilos",
"stroke": "Contornos",
"background": "Fundo",
"fill": "Preenchimento",
"strokeWidth": "Espessura dos contornos",
"sloppiness": "Desleixo",
"opacity": "Opacidade",
"fontSize": "Tamanho da fonte",
"fontFamily": "Fonte",
"onlySelected": "Somente a seleção",
"withBackground": "Com fundo",
"handDrawn": "Manuscrito",
"normal": "Normal",
"code": "Código",
"small": "Pequeno",
"medium": "Médio",
"large": "Grande",
"veryLarge": "Muito Grande",
"solid": "Sólido",
"hachure": "Eclosão",
"crossHatch": "Eclosão cruzada",
"thin": "Fino",
"bold": "Espesso",
"extraBold": "Muito espesso",
"architect": "Arquitecto",
"artist": "Artista",
"cartoonist": "Caricaturista"
},
"buttons": {
"clearReset": "Limpar o canvas e redefinir a cor de fundo",
"export": "Exportar",
"exportToPng": "Exportar em PNG",
"exportToSvg": "Exportar em SVG",
"copyToClipboard": "Copiar para o clipboard",
"save": "Guardar",
"load": "Carregar",
"getShareableLink": "Obter um link de partilha",
"previouslyLoadedScenes": "Cenas carregadas anteriormente"
},
"alerts": {
"clearReset": "O canvas inteiro será excluído. Tens a certeza?",
"couldNotCreateShareableLink": "Não foi possível criar um link de partilha.",
"importBackendFailed": "O carregamento no servidor falhou.",
"cannotExportEmptyCanvas": "Não é possível exportar um canvas vazío.",
"couldNotCopyToClipboard": "Não foi possível copiar no clipboard. Experimente no navegador Chrome.",
"copiedToClipboard": "Copiado no clipboard: {{url}}"
},
"toolBar": {
"selection": "Seleção",
"rectangle": "Retângulo",
"diamond": "Losango",
"ellipse": "Elipse",
"arrow": "Flecha",
"line": "Linha",
"text": "Texto"
}
}

80
src/locales/ru.json Normal file
View file

@ -0,0 +1,80 @@
{
"labels": {
"paste": "Вставить",
"selectAll": "Выделить всё",
"copy": "Копировать",
"bringForward": "Переложить вперёд",
"sendToBack": "На задний план",
"bringToFront": "На передний план",
"sendBackward": "Переложить назад",
"delete": "Удалить",
"copyStyles": "Скопировать стили",
"pasteStyles": "Вставить стили",
"stroke": "Обводка",
"background": "Фон",
"fill": "Заливка",
"strokeWidth": "Толщина обводки",
"sloppiness": "Стиль обводки",
"opacity": "Непрозрачность",
"fontSize": "Размер шрифта",
"fontFamily": "Семейство шрифта",
"onlySelected": "Только выбранные",
"withBackground": "с фоном",
"handDrawn": "Нарисованный от руки",
"normal": "Обычный",
"code": "Код",
"small": "Малый",
"medium": "Средний",
"large": "Большой",
"veryLarge": "Очень Большой",
"solid": "Однотонная",
"hachure": "Штрихованная",
"crossHatch": "Перекрестная",
"thin": "Тонкая",
"bold": "Жирная",
"extraBold": "Очень Жирная",
"architect": "Архитектор",
"artist": "Художник",
"cartoonist": "Карикатурист",
"fileTitle": "Название файла",
"colorPicker": "Выбор цвета",
"canvasBackground": "Фон холста",
"drawingCanvas": "Рисование холста"
},
"buttons": {
"clearReset": "Очистить холст & сбросить цвет фона",
"export": "Экспортировать",
"exportToPng": "Экспорт в PNG",
"exportToSvg": "Экспорт в SVG",
"copyToClipboard": "Скопировать в буфер обмена",
"save": "Сохранить",
"load": "Загрузить",
"getShareableLink": "Получить доступ по ссылке",
"close": "Закрыть",
"selectLanguage": "Выбрать язык",
"previouslyLoadedScenes": "Ранее загруженные сцены"
},
"alerts": {
"clearReset": "Это очистит весь холст. Вы уверены?",
"couldNotCreateShareableLink": "Не удалось создать общедоступную ссылку.",
"importBackendFailed": "Не удалось импортировать из бэкэнда.",
"cannotExportEmptyCanvas": "Не может экспортировать пустой холст.",
"couldNotCopyToClipboard": "Не удалось скопировать в буфер обмена. Попробуйте использовать веб-браузер Chrome.",
"copiedToClipboard": "Скопировано в буфер обмена: {{url}}"
},
"toolBar": {
"selection": "Выделение области",
"rectangle": "Прямоугольник",
"diamond": "Ромб",
"ellipse": "Эллипс",
"arrow": "Cтрелка",
"line": "Линия",
"text": "Текст",
"lock": "Сохранять выбранный инструмент активным после рисования"
},
"headings": {
"canvasActions": "Операции холста",
"selectedShapeActions": "Операции выбранной фигуры",
"shapes": "Фигуры"
}
}

View file

@ -9,7 +9,7 @@ import nanoid from "nanoid";
import { fileOpen, fileSave } from "browser-nativefs";
import { getCommonBounds } from "../element";
import i18n from "../i18n";
import { t } from "../i18n";
const LOCAL_STORAGE_KEY = "excalidraw";
const LOCAL_STORAGE_SCENE_PREVIOUS_KEY = "excalidraw-previos-scenes";
@ -142,16 +142,15 @@ export async function exportToBackend(
await navigator.clipboard.writeText(url.toString());
window.alert(
i18n.t("alerts.copiedToClipboard", {
t("alerts.copiedToClipboard", {
url: url.toString(),
interpolation: { escapeValue: false },
}),
);
} else {
window.alert(i18n.t("alerts.couldNotCreateShareableLink"));
window.alert(t("alerts.couldNotCreateShareableLink"));
}
} catch (e) {
window.alert(i18n.t("alerts.couldNotCreateShareableLink"));
window.alert(t("alerts.couldNotCreateShareableLink"));
return;
}
}
@ -167,7 +166,7 @@ export async function importFromBackend(id: string | null) {
elements = response.elements || elements;
appState = response.appState || appState;
} catch (error) {
window.alert(i18n.t("alerts.importBackendFailed"));
window.alert(t("alerts.importBackendFailed"));
console.error(error);
}
}
@ -193,7 +192,7 @@ export async function exportCanvas(
},
) {
if (!elements.length)
return window.alert(i18n.t("alerts.cannotExportEmptyCanvas"));
return window.alert(t("alerts.cannotExportEmptyCanvas"));
// calculate smallest area to fit the contents in
if (type === "svg") {
@ -227,7 +226,7 @@ export async function exportCanvas(
}
});
} else if (type === "clipboard") {
const errorMsg = i18n.t("alerts.couldNotCopyToClipboard");
const errorMsg = t("alerts.couldNotCopyToClipboard");
try {
tempCanvas.toBlob(async function(blob: any) {
try {