Compare commits

..

8 Commits

Author SHA1 Message Date
dwelle c9f62f20d5 debuggs 2026-03-09 22:04:43 +01:00
David Luzar 21dd1cfacc feat(packages/excalidraw): state tracking, api hook, and others (#10870) 2026-03-08 23:15:18 +01:00
David Luzar fa1f7d9f22 feat(packages/excalidraw): export throttleRAF (#10912) 2026-03-07 12:05:33 +01:00
David Luzar 3d8c12fba4 fix(editor): do not conditionally disable midpoint snapping menu preference (#10906) 2026-03-06 20:44:57 +01:00
David Luzar 757dfeb6ad fix(editor): call throttleRAF with lastArgs and remove trailing (#10905)
Co-authored-by: Varun Chawla <varun_6april@hotmail.com>
Co-authored-by: aziamimoh <aziamimoh@users.noreply.github.com>
Co-authored-by: pgzcoa <pgzcoa@users.noreply.github.com>
Co-authored-by: TinaZhang24 <TinaZhang24@users.noreply.github.com>
2026-03-06 20:40:36 +01:00
David Luzar a0e93b6040 feat(editor): sync export theme with ui theme (#10903) 2026-03-06 18:37:28 +01:00
Hendrik Horstmann 499e9d64a5 fix: dropdownMenu item badge position (#10895) 2026-03-06 08:41:49 +00:00
David Luzar c1dbbdf678 feat(editor): mermaid code editor & improve parsing (#10897) 2026-03-05 18:52:41 +01:00
130 changed files with 4426 additions and 2428 deletions
+22 -1
View File
@@ -39,5 +39,26 @@
"allowReferrer": true
}
]
}
},
"overrides": [
{
"files": ["packages/excalidraw/**/*.{ts,tsx}"],
"excludedFiles": ["packages/excalidraw/**/*.test.{ts,tsx}", "packages/excalidraw/**/*.test.*.{ts,tsx}"],
"rules": {
"@typescript-eslint/no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["@excalidraw/excalidraw"],
"message": "Do not import from the barrel 'index.tsx' files. Use direct relative imports to the specific module instead.",
"allowTypeImports": true
}
],
"paths": [".", "..", "../..", "../../..", "../../../..", "../../../../..", "../index", "../../index", "../../../index", "../../../../index"]
}
]
}
}
]
}
+5 -5
View File
@@ -3,14 +3,14 @@
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "19.0.0",
"react-dom": "19.0.0",
"@excalidraw/excalidraw": "*",
"browser-fs-access": "0.29.1"
"browser-fs-access": "0.38.0",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"vite": "5.0.12",
"typescript": "^5"
"typescript": "^5",
"vite": "5.0.12"
},
"scripts": {
"start": "vite",
-36
View File
@@ -4,8 +4,6 @@ import { unstable_batchedUpdates } from "react-dom";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
const INPUT_CHANGE_INTERVAL_MS = 500;
export type ResolvablePromise<T> = Promise<T> & {
resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
reject: (error: Error) => void;
@@ -54,40 +52,6 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
extensions,
mimeTypes,
multiple: opts.multiple ?? false,
legacySetup: (resolve, reject, input) => {
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
const focusHandler = () => {
checkForFile();
document.addEventListener("keyup", scheduleRejection);
document.addEventListener("pointerup", scheduleRejection);
scheduleRejection();
};
const checkForFile = () => {
// this hack might not work when expecting multiple files
if (input.files?.length) {
const ret = opts.multiple ? [...input.files] : input.files[0];
resolve(ret as RetType);
}
};
requestAnimationFrame(() => {
window.addEventListener("focus", focusHandler);
});
const interval = window.setInterval(() => {
checkForFile();
}, INPUT_CHANGE_INTERVAL_MS);
return (rejectPromise) => {
clearInterval(interval);
scheduleRejection.cancel();
window.removeEventListener("focus", focusHandler);
document.removeEventListener("keyup", scheduleRejection);
document.removeEventListener("pointerup", scheduleRejection);
if (rejectPromise) {
// so that something is shown in console if we need to debug this
console.warn("Opening the file was canceled (legacy-fs).");
rejectPromise(new Error("Request Aborted"));
}
};
},
}) as Promise<RetType>;
};
+92 -20
View File
@@ -5,6 +5,8 @@ import {
CaptureUpdateAction,
reconcileElements,
useEditorInterface,
ExcalidrawAPIProvider,
useExcalidrawAPI,
} from "@excalidraw/excalidraw";
import { trackEvent } from "@excalidraw/excalidraw/analytics";
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
@@ -34,7 +36,6 @@ import {
import polyfill from "@excalidraw/excalidraw/polyfill";
import { useCallback, useEffect, useRef, useState } from "react";
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
import { t } from "@excalidraw/excalidraw/i18n";
import {
@@ -74,6 +75,7 @@ import type {
BinaryFiles,
ExcalidrawInitialDataState,
UIAppState,
ExcalidrawProps,
} from "@excalidraw/excalidraw/types";
import type { ResolutionType } from "@excalidraw/common/utility-types";
import type { ResolvablePromise } from "@excalidraw/common/utils";
@@ -114,6 +116,7 @@ import {
} from "./data";
import { updateStaleImageStatuses } from "./data/FileManager";
import { FileStatusStore } from "./data/fileStatusStore";
import {
importFromLocalStorage,
importUsernameFromLocalStorage,
@@ -369,6 +372,8 @@ const initializeScene = async (opts: {
};
const ExcalidrawWrapper = () => {
const excalidrawAPI = useExcalidrawAPI();
const [errorMessage, setErrorMessage] = useState("");
const isCollabDisabled = isRunningInIframe();
@@ -399,9 +404,6 @@ const ExcalidrawWrapper = () => {
}, VERSION_TIMEOUT);
}, []);
const [excalidrawAPI, excalidrawRefCallback] =
useCallbackRefState<ExcalidrawImperativeAPI>();
const [, setShareDialogState] = useAtom(shareDialogStateAtom);
const [collabAPI] = useAtom(collabAPIAtom);
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
@@ -433,18 +435,15 @@ const ExcalidrawWrapper = () => {
}
}, [excalidrawAPI]);
useEffect(() => {
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
return;
}
const loadImages = (
data: ResolutionType<typeof initializeScene>,
isInitialLoad = false,
) => {
if (!data.scene) {
// ---------------------------------------------------------------------------
// Hoisted loadImages
// ---------------------------------------------------------------------------
const loadImages = useCallback(
(data: ResolutionType<typeof initializeScene>, isInitialLoad = false) => {
if (!data.scene || !excalidrawAPI) {
return;
}
if (collabAPI?.isCollaborating()) {
if (data.scene.elements) {
collabAPI
@@ -471,6 +470,12 @@ const ExcalidrawWrapper = () => {
}, [] as FileId[]) || [];
if (data.isExternalScene) {
if (fileIds.length) {
// Direct Firebase call (not through FileManager), so track manually
FileStatusStore.updateStatuses(
fileIds.map((id) => [id, "loading"]),
);
}
loadFilesFromFirebase(
`${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`,
data.key,
@@ -482,12 +487,18 @@ const ExcalidrawWrapper = () => {
erroredFiles,
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
});
FileStatusStore.updateStatuses([
...loadedFiles.map((f) => [f.id, "loaded"] as [FileId, "loaded"]),
...[...erroredFiles.keys()].map(
(id) => [id, "error"] as [FileId, "error"],
),
]);
});
} else if (isInitialLoad) {
if (fileIds.length) {
LocalData.fileStorage
.getFiles(fileIds)
.then(({ loadedFiles, erroredFiles }) => {
.then(async ({ loadedFiles, erroredFiles }) => {
if (loadedFiles.length) {
excalidrawAPI.addFiles(loadedFiles);
}
@@ -500,10 +511,19 @@ const ExcalidrawWrapper = () => {
}
// on fresh load, clear unused files from IDB (from previous
// session)
LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
LocalData.fileStorage.clearObsoleteFiles({
currentFileIds: fileIds,
});
}
}
};
},
[collabAPI, excalidrawAPI],
);
useEffect(() => {
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
return;
}
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
loadImages(data, /* isInitialLoad */ true);
@@ -628,7 +648,7 @@ const ExcalidrawWrapper = () => {
false,
);
};
}, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);
}, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode, loadImages]);
useEffect(() => {
const unloadHandler = (event: BeforeUnloadEvent) => {
@@ -773,6 +793,56 @@ const ExcalidrawWrapper = () => {
[setShareDialogState],
);
// ---------------------------------------------------------------------------
// onExport — intercepts file save to wait for pending image loads
// ---------------------------------------------------------------------------
const onExport: Required<ExcalidrawProps>["onExport"] = useCallback(
async function* () {
let snapshot = FileStatusStore.getSnapshot();
const { pending, total } = FileStatusStore.getPendingCount(
snapshot.value,
);
if (pending === 0) {
return;
}
// Yield initial progress
yield {
type: "progress",
progress: (total - pending) / total,
message: `Loading images (${total - pending}/${total})...`,
};
// Wait for all pending images to finish
while (true) {
snapshot = await FileStatusStore.pull(snapshot.version);
const { pending: nowPending, total: nowTotal } =
FileStatusStore.getPendingCount(snapshot.value);
yield {
type: "progress",
progress: (nowTotal - nowPending) / nowTotal,
message: `Loading images (${nowTotal - nowPending}/${nowTotal})...`,
};
if (nowPending === 0) {
await new Promise((r) => setTimeout(r, 500));
yield {
type: "progress",
message: `Preparing export...`,
};
return;
}
}
},
[],
);
// const onExport = () => {
// return new Promise((r) => setTimeout(r, 2500));
// // console.log("onExport");
// };
// browsers generally prevent infinite self-embedding, there are
// cases where it still happens, and while we disallow self-embedding
// by not whitelisting our own origin, this serves as an additional guard
@@ -839,8 +909,8 @@ const ExcalidrawWrapper = () => {
})}
>
<Excalidraw
excalidrawAPI={excalidrawRefCallback}
onChange={onChange}
onExport={onExport}
initialData={initialStatePromiseRef.current.promise}
isCollaborating={isCollaborating}
onPointerUpdate={collabAPI?.onPointerUpdate}
@@ -1206,7 +1276,9 @@ const ExcalidrawApp = () => {
return (
<TopErrorBoundary>
<Provider store={appJotaiStore}>
<ExcalidrawWrapper />
<ExcalidrawAPIProvider>
<ExcalidrawWrapper />
</ExcalidrawAPIProvider>
</Provider>
</TopErrorBoundary>
);
+2
View File
@@ -72,6 +72,7 @@ import {
FileManager,
updateStaleImageStatuses,
} from "../data/FileManager";
import { FileStatusStore } from "../data/fileStatusStore";
import { LocalData } from "../data/LocalData";
import {
isSavedToFirebase,
@@ -149,6 +150,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
};
this.portal = new Portal(this);
this.fileManager = new FileManager({
onFileStatusChange: FileStatusStore.updateStatuses.bind(FileStatusStore),
getFiles: async (fileIds) => {
const { roomId, roomKey } = this.portal;
if (!roomId || !roomKey) {
@@ -414,7 +414,6 @@ export const debugRenderer = throttleRAF(
) => {
_debugRenderer(canvas, appState, elements, scale);
},
{ trailing: true },
);
export const loadSavedDebugState = () => {
+22
View File
@@ -40,10 +40,12 @@ export class FileManager {
private _getFiles;
private _saveFiles;
private _onFileStatusChange;
constructor({
getFiles,
saveFiles,
onFileStatusChange,
}: {
getFiles: (fileIds: FileId[]) => Promise<{
loadedFiles: BinaryFileData[];
@@ -53,9 +55,13 @@ export class FileManager {
savedFiles: Map<FileId, BinaryFileData>;
erroredFiles: Map<FileId, BinaryFileData>;
}>;
onFileStatusChange?: (
updates: Array<[FileId, "loading" | "loaded" | "error"]>,
) => void;
}) {
this._getFiles = getFiles;
this._saveFiles = saveFiles;
this._onFileStatusChange = onFileStatusChange;
}
/**
@@ -146,6 +152,8 @@ export class FileManager {
this.fetchingFiles.set(id, true);
}
this._onFileStatusChange?.(ids.map((id) => [id, "loading"]));
try {
const { loadedFiles, erroredFiles } = await this._getFiles(ids);
@@ -156,6 +164,13 @@ export class FileManager {
this.erroredFiles_fetch.set(fileId, true);
}
this._onFileStatusChange?.([
...loadedFiles.map((f) => [f.id, "loaded"] as [FileId, "loaded"]),
...[...erroredFiles.keys()].map(
(id) => [id, "error"] as [FileId, "error"],
),
]);
return { loadedFiles, erroredFiles };
} finally {
for (const id of ids) {
@@ -195,6 +210,13 @@ export class FileManager {
};
reset() {
if (this._onFileStatusChange && this.fetchingFiles.size) {
this._onFileStatusChange(
[...this.fetchingFiles.keys()].map(
(id) => [id, "error"] as [FileId, "error"],
),
);
}
this.fetchingFiles.clear();
this.savingFiles.clear();
this.savedFiles.clear();
+2
View File
@@ -42,6 +42,7 @@ import type { MaybePromise } from "@excalidraw/common/utility-types";
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
import { FileManager } from "./FileManager";
import { FileStatusStore } from "./fileStatusStore";
import { Locker } from "./Locker";
import { updateBrowserStateVersion } from "./tabSync";
@@ -166,6 +167,7 @@ export class LocalData {
// ---------------------------------------------------------------------------
static fileStorage = new LocalFileManager({
onFileStatusChange: FileStatusStore.updateStatuses.bind(FileStatusStore),
getFiles(ids) {
return getMany(ids, filesStore).then(
async (filesData: (BinaryFileData | undefined)[]) => {
+48
View File
@@ -0,0 +1,48 @@
import { VersionedSnapshotStore } from "@excalidraw/common";
import type { FileId } from "@excalidraw/element/types";
export type FileLoadingStatus = "loading" | "loaded" | "error";
export class FileStatusStore {
private static store = new VersionedSnapshotStore<
Map<FileId, FileLoadingStatus>
>(new Map());
static getSnapshot() {
return this.store.getSnapshot();
}
static pull(sinceVersion?: number) {
return this.store.pull(sinceVersion);
}
static updateStatuses(updates: Array<[FileId, FileLoadingStatus]>) {
if (!updates.length) {
return;
}
this.store.update((prev) => {
let changed = false;
const next = new Map(prev);
for (const [id, status] of updates) {
if (next.get(id) !== status) {
next.set(id, status);
changed = true;
}
}
return changed ? next : prev;
});
}
static getPendingCount(statuses: Map<FileId, FileLoadingStatus>) {
let pending = 0;
let total = 0;
for (const status of statuses.values()) {
total++;
if (status === "loading") {
pending++;
}
}
return { pending, total };
}
}
+10 -1
View File
@@ -106,6 +106,10 @@ export default defineConfig(({ mode }) => {
if (id.includes("@excalidraw/mermaid-to-excalidraw")) {
return "mermaid-to-excalidraw";
}
if (id.includes("@codemirror/") || id.includes("@lezer/")) {
return "codemirror.chunk";
}
},
},
},
@@ -150,6 +154,11 @@ export default defineConfig(({ mode }) => {
"**/locales/**",
"service-worker.js",
"**/*.chunk-*.js",
// CodeMirrorEditor can't be assigned a `.chunk` name via
// manualChunks because Rollup would hoist shared deps (React)
// via a static import from the main bundle, defeating lazy
// loading. So we exclude it by name instead.
"**/CodeMirrorEditor-*.js",
],
runtimeCaching: [
{
@@ -189,7 +198,7 @@ export default defineConfig(({ mode }) => {
},
},
{
urlPattern: new RegExp(".chunk-.+.js"),
urlPattern: new RegExp("(.chunk-.+|CodeMirrorEditor-.+)\\.js"),
handler: "CacheFirst",
options: {
cacheName: "chunk",
+74
View File
@@ -0,0 +1,74 @@
import { AppEventBus } from "./appEventBus";
type TestEvents = {
initialize: [api: number];
pointerUp: [pointerId: string];
viewState: [zoom: number];
};
const behavior = {
initialize: { cardinality: "once", replay: "last" },
pointerUp: { cardinality: "many", replay: "none" },
viewState: { cardinality: "many", replay: "last" },
} as const;
const flushMicrotasks = async () => Promise.resolve();
describe("AppEventBus", () => {
it("replays once events to late callback and Promise subscribers", async () => {
const bus = new AppEventBus<TestEvents, typeof behavior>(behavior);
bus.emit("initialize", 42);
const calls: number[] = [];
bus.on("initialize", (value) => {
calls.push(value);
});
expect(calls).toEqual([]);
await flushMicrotasks();
expect(calls).toEqual([42]);
await expect(bus.on("initialize")).resolves.toBe(42);
});
it("does not replay stream events to late subscribers", async () => {
const bus = new AppEventBus<TestEvents, typeof behavior>(behavior);
bus.emit("pointerUp", "first");
const calls: string[] = [];
bus.on("pointerUp", (pointerId) => {
calls.push(pointerId);
});
await flushMicrotasks();
expect(calls).toEqual([]);
bus.emit("pointerUp", "second");
expect(calls).toEqual(["second"]);
});
it("replays replay-last stream events and stays subscribed", async () => {
const bus = new AppEventBus<TestEvents, typeof behavior>(behavior);
bus.emit("viewState", 1);
const calls: number[] = [];
bus.on("viewState", (zoom) => {
calls.push(zoom);
});
await flushMicrotasks();
expect(calls).toEqual([1]);
bus.emit("viewState", 2);
expect(calls).toEqual([1, 2]);
});
it("throws when emitting a once event twice", () => {
const bus = new AppEventBus<TestEvents, typeof behavior>(behavior);
bus.emit("initialize", 1);
expect(() => {
bus.emit("initialize", 2);
}).toThrow('Event "initialize" can only be emitted once');
});
});
+136
View File
@@ -0,0 +1,136 @@
import type { UnsubscribeCallback } from "@excalidraw/excalidraw/types";
import { Emitter } from "./emitter";
import { isProdEnv } from "./utils";
export type AppEventPayloadMap = Record<string, unknown[]>;
export type AppEventBehavior = {
cardinality: "once" | "many";
replay: "none" | "last";
};
export type AppEventBehaviorMap<Events extends AppEventPayloadMap> = {
[K in keyof Events]: AppEventBehavior;
};
type AwaitableAppEventKeys<
Events extends AppEventPayloadMap,
Behavior extends AppEventBehaviorMap<Events>,
> = {
[K in keyof Events]: Behavior[K]["cardinality"] extends "once"
? Behavior[K]["replay"] extends "last"
? K
: never
: never;
}[keyof Events];
type AppEventPromiseValue<Args extends any[]> = Args extends [infer Only]
? Only
: Args;
export class AppEventBus<
Events extends AppEventPayloadMap,
Behavior extends AppEventBehaviorMap<Events>,
> {
private readonly emitters = new Map<keyof Events, Emitter<any>>();
private readonly lastPayload = new Map<keyof Events, any[]>();
private readonly emittedOnce = new Set<keyof Events>();
constructor(private readonly behavior: Behavior) {}
private getEmitter<K extends keyof Events>(name: K): Emitter<Events[K]> {
let emitter = this.emitters.get(name);
if (!emitter) {
emitter = new Emitter<any>();
this.emitters.set(name, emitter);
}
return emitter as Emitter<Events[K]>;
}
private toPromiseValue<Args extends any[]>(
args: Args,
): AppEventPromiseValue<Args> {
return (args.length === 1 ? args[0] : args) as AppEventPromiseValue<Args>;
}
public on<K extends keyof Events>(
name: K,
callback: (...args: Events[K]) => void,
): UnsubscribeCallback;
public on<K extends AwaitableAppEventKeys<Events, Behavior>>(
name: K,
): Promise<AppEventPromiseValue<Events[K]>>;
public on<K extends keyof Events>(
name: K,
callback?: (...args: Events[K]) => void,
): UnsubscribeCallback | Promise<AppEventPromiseValue<Events[K]>> {
const eventBehavior = this.behavior[name];
const cachedPayload = this.lastPayload.get(name) as Events[K] | undefined;
if (callback) {
if (eventBehavior.replay === "last" && cachedPayload) {
queueMicrotask(() => callback(...cachedPayload));
if (eventBehavior.cardinality === "once") {
return () => {};
}
}
return this.getEmitter(name).on(callback);
}
if (
eventBehavior.cardinality !== "once" ||
eventBehavior.replay !== "last"
) {
throw new Error(`Event "${String(name)}" requires a callback`);
}
if (cachedPayload) {
return Promise.resolve(this.toPromiseValue(cachedPayload));
}
return new Promise<AppEventPromiseValue<Events[K]>>((resolve) => {
this.getEmitter(name).once((...args: Events[K]) => {
resolve(this.toPromiseValue(args));
});
});
}
public emit<K extends keyof Events>(name: K, ...args: Events[K]) {
const eventBehavior = this.behavior[name];
if (!isProdEnv()) {
if (eventBehavior.cardinality === "once") {
if (this.emittedOnce.has(name)) {
throw new Error(`Event "${String(name)}" can only be emitted once`);
}
this.emittedOnce.add(name);
}
}
if (eventBehavior.replay === "last") {
this.lastPayload.set(name, args);
}
try {
this.getEmitter(name).trigger(...args);
} finally {
if (eventBehavior.cardinality === "once") {
this.getEmitter(name).clear();
}
}
}
public clear() {
this.lastPayload.clear();
this.emittedOnce.clear();
for (const emitter of this.emitters.values()) {
emitter.clear();
}
this.emitters.clear();
}
}
+2
View File
@@ -11,5 +11,7 @@ export * from "./random";
export * from "./url";
export * from "./utils";
export * from "./emitter";
export * from "./appEventBus";
export * from "./editorInterface";
export * from "./versionedSnapshotStore";
export { Debug } from "../debug";
+89
View File
@@ -3,6 +3,12 @@ import {
mapFind,
reduceToCommonValue,
} from "@excalidraw/common";
import { vi } from "vitest";
// Import directly to avoid the @excalidraw/common throttleRAF mock from setupTests.ts.
import { throttleRAF } from "./utils";
type RafCallback = FrameRequestCallback;
describe("@excalidraw/common/utils", () => {
describe("isTransparent()", () => {
@@ -79,4 +85,87 @@ describe("@excalidraw/common/utils", () => {
expect(mapFind([1, 2], () => null)).toBe(undefined);
});
});
describe("throttleRAF()", () => {
let frameCallbacks: Map<number, RafCallback>;
let nextFrameId: number;
const runScheduledFrame = (timestamp = 16) => {
const callbacks = [...frameCallbacks.values()];
frameCallbacks.clear();
callbacks.forEach((callback) => callback(timestamp));
};
beforeEach(() => {
frameCallbacks = new Map();
nextFrameId = 0;
vi.spyOn(window, "requestAnimationFrame").mockImplementation(
(callback) => {
const frameId = ++nextFrameId;
frameCallbacks.set(frameId, callback);
return frameId;
},
);
vi.spyOn(window, "cancelAnimationFrame").mockImplementation((frameId) => {
frameCallbacks.delete(frameId);
});
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should invoke the callback with the last args from the same frame", () => {
const fn = vi.fn();
const throttled = throttleRAF(fn);
throttled("first", 1);
throttled("second", 2);
throttled("last", 3);
expect(fn).not.toHaveBeenCalled();
expect(window.requestAnimationFrame).toHaveBeenCalledTimes(1);
runScheduledFrame();
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("last", 3);
});
it("should flush the pending callback immediately", () => {
const fn = vi.fn();
const throttled = throttleRAF(fn);
throttled("first");
throttled("last");
throttled.flush();
expect(window.cancelAnimationFrame).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("last");
runScheduledFrame();
expect(fn).toHaveBeenCalledTimes(1);
});
it("should cancel the pending callback", () => {
const fn = vi.fn();
const throttled = throttleRAF(fn);
throttled("first");
throttled("last");
throttled.cancel();
expect(window.cancelAnimationFrame).toHaveBeenCalledTimes(1);
runScheduledFrame();
expect(fn).not.toHaveBeenCalled();
});
});
});
+13 -23
View File
@@ -88,7 +88,8 @@ export const isWritableElement = (
(target.type === "text" ||
target.type === "number" ||
target.type === "password" ||
target.type === "search"));
target.type === "search")) ||
(target instanceof HTMLElement && target.closest(".cm-editor") !== null);
export const getFontFamilyString = ({
fontFamily,
@@ -150,38 +151,27 @@ export const debounce = <T extends any[]>(
return ret;
};
// throttle callback to execute once per animation frame
export const throttleRAF = <T extends any[]>(
fn: (...args: T) => void,
opts?: { trailing?: boolean },
) => {
// throttle callback to execute once per animation frame using the latest args
export const throttleRAF = <T extends any[]>(fn: (...args: T) => void) => {
let timerId: number | null = null;
let lastArgs: T | null = null;
let lastArgsTrailing: T | null = null;
const scheduleFunc = (args: T) => {
const scheduleFunc = () => {
timerId = window.requestAnimationFrame(() => {
timerId = null;
fn(...args);
const args = lastArgs;
lastArgs = null;
if (lastArgsTrailing) {
lastArgs = lastArgsTrailing;
lastArgsTrailing = null;
scheduleFunc(lastArgs);
if (args) {
fn(...args);
}
});
};
const ret = (...args: T) => {
if (isTestEnv()) {
fn(...args);
return;
}
lastArgs = args;
if (timerId === null) {
scheduleFunc(lastArgs);
} else if (opts?.trailing) {
lastArgsTrailing = args;
scheduleFunc();
}
};
ret.flush = () => {
@@ -190,12 +180,12 @@ export const throttleRAF = <T extends any[]>(
timerId = null;
}
if (lastArgs) {
fn(...(lastArgsTrailing || lastArgs));
lastArgs = lastArgsTrailing = null;
fn(...lastArgs);
lastArgs = null;
}
};
ret.cancel = () => {
lastArgs = lastArgsTrailing = null;
lastArgs = null;
if (timerId !== null) {
cancelAnimationFrame(timerId);
timerId = null;
@@ -0,0 +1,70 @@
export type VersionedSnapshot<T> = Readonly<{
version: number;
value: T;
}>;
export class VersionedSnapshotStore<T> {
private version = 0;
private value: T;
private readonly waiters = new Set<
(snapshot: VersionedSnapshot<T>) => void
>();
private readonly subscribers = new Set<
(snapshot: VersionedSnapshot<T>) => void
>();
constructor(
initialValue: T,
private readonly isEqual: (prev: T, next: T) => boolean = Object.is,
) {
this.value = initialValue;
}
public getSnapshot(): VersionedSnapshot<T> {
return { version: this.version, value: this.value };
}
public set(nextValue: T): boolean {
if (this.isEqual(this.value, nextValue)) {
return false;
}
this.value = nextValue;
this.version += 1;
const snapshot = this.getSnapshot();
for (const subscriber of this.subscribers) {
subscriber(snapshot);
}
for (const waiter of this.waiters) {
waiter(snapshot);
}
this.waiters.clear();
return true;
}
public update(updater: (prev: T) => T): boolean {
return this.set(updater(this.value));
}
public subscribe(
subscriber: (snapshot: VersionedSnapshot<T>) => void,
): () => void {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
}
public pull(sinceVersion = -1): Promise<VersionedSnapshot<T>> {
if (this.version !== sinceVersion) {
return Promise.resolve(this.getSnapshot());
}
return new Promise((resolve) => {
this.waiters.add(resolve);
});
}
}
+86
View File
@@ -11,6 +11,92 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section.
-->
## Unreleased
## Excalidraw API
### Breaking changes
- Renamed the `excalidrawAPI` prop to `onExcalidrawAPI`.
### Features
- Added `onMount` and `onInitialize` props. `onMount` receives `{ excalidrawAPI, container }` once the editor root is mounted, and `onInitialize` fires once the initial scene has loaded.
```tsx
<Excalidraw
onMount={({ excalidrawAPI, container }) => {
console.log(container);
excalidrawAPI.scrollToContent();
}}
onInitialize={(api) => {
api.refresh();
}}
/>
```
- Same events are also accessible imperatively through `api.onEvent(...)`.
```tsx
<Excalidraw
onExcalidrawAPI={(api) => {
api.onEvent("editor:mount", ({ excalidrawAPI, container }) => {
excalidrawAPI.scrollToContent();
console.log(container);
});
api.onEvent("editor:initialize").then((readyApi) => {
readyApi.scrollToContent();
});
}}
/>
```
Note that in future releases, most, if not all, `excalidrawAPI.on*` subscriptions will be removed in favor of `excalidrawAPI.onEvent(name)`.
- Exported `ExcalidrawAPIProvider`, `useExcalidrawAPI`, and `useAppStateValue` from the package entrypoint. The imperative API also now exposes `onStateChange`.
```tsx
<ExcalidrawAPIProvider>
<Excalidraw />
<Logger />
</ExcalidrawAPIProvider>;
function Logger() {
const api = useExcalidrawAPI();
useAppStateValue("viewModeEnabled", (viewModeEnabled) => {
console.log("view mode changed:", viewModeEnabled);
});
React.useEffect(() => {
if (api) {
console.log("editor instance id:", api.id);
}
}, [api]);
return null;
}
```
- Added `onExport` so host apps can delay JSON export until async work completes. The handler receives the export data plus an `AbortSignal`, and may return a `Promise` or an async generator that yields progress updates for the built-in toast UI.
```tsx
<Excalidraw
onExport={async function* (_type, { files }, { signal }) {
yield { type: "progress", message: "Waiting for images..." };
await waitForImagesToLoad(files, signal);
if (signal.aborted) {
return;
}
yield { type: "progress", message: "Export ready", progress: 1 };
}}
/>
```
## Excalidraw Library
## 0.18.0 (2025-03-11)
@@ -30,7 +30,7 @@ import { getSelectedElements, isSomeElementSelected } from "../scene";
import { TrashIcon } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { useStylesPanelMode } from "..";
import { useStylesPanelMode } from "../components/App";
import { register } from "./register";
@@ -27,7 +27,7 @@ import { t } from "../i18n";
import { isSomeElementSelected } from "../scene";
import { getShortcutKey } from "../shortcut";
import { useStylesPanelMode } from "..";
import { useStylesPanelMode } from "../components/App";
import { register } from "./register";
+219 -37
View File
@@ -9,18 +9,20 @@ import { getNonDeletedElements } from "@excalidraw/element";
import { CaptureUpdateAction } from "@excalidraw/element";
import type { Theme } from "@excalidraw/element/types";
import type { ExcalidrawElement, Theme } from "@excalidraw/element/types";
import { useEditorInterface } from "../components/App";
import { CheckboxItem } from "../components/CheckboxItem";
import { DarkModeToggle } from "../components/DarkModeToggle";
import { ProjectName } from "../components/ProjectName";
import { Toast } from "../components/Toast";
import { ToolButton } from "../components/ToolButton";
import { Tooltip } from "../components/Tooltip";
import { ExportIcon, questionCircle, saveAs } from "../components/icons";
import { loadFromJSON, saveAsJSON } from "../data";
import { isImageFileHandle } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import { resaveAsImageWithScene } from "../data/resave";
import { t } from "../i18n";
@@ -31,7 +33,15 @@ import "../components/ToolIcon.scss";
import { register } from "./register";
import type { AppState } from "../types";
import type { JSONExportData } from "../data/json";
import type {
AppClassProperties,
AppState,
BinaryFiles,
ExcalidrawProps,
OnExportProgress,
} from "../types";
export const actionChangeProjectName = register<AppState["name"]>({
name: "changeProjectName",
@@ -150,6 +160,143 @@ export const actionChangeExportEmbedScene = register<
),
});
// ---------------------------------------------------------------------------
// onExport interception helpers
// ---------------------------------------------------------------------------
let onExportInProgress = false;
const onProgressToast = (
app: AppClassProperties,
progress: {
message?: OnExportProgress["message"];
progress?: number | null;
},
) => {
const message = progress.message ?? t("progressDialog.defaultMessage");
app.setAppState({
toast: {
message:
progress.progress != null ? (
<>
{message}
<Toast.ProgressBar progress={progress.progress} />
</>
) : (
message
),
duration: Infinity,
},
});
};
/** awaits host app's onExport result, and renders progress to the UI */
async function handleOnExportResult(
onExportResult: ReturnType<NonNullable<ExcalidrawProps["onExport"]>>,
opts: {
signal: AbortSignal;
app: AppClassProperties;
},
): Promise<void> {
if (opts.app.state.isLoading) {
onProgressToast(opts.app, { progress: null });
await opts.app.onStateChange({ predicate: (state) => !state.isLoading });
}
if (
onExportResult != null &&
typeof onExportResult === "object" &&
Symbol.asyncIterator in onExportResult
) {
for await (const value of onExportResult) {
if (opts.signal.aborted) {
onExportResult.return();
return;
}
if (value.type === "progress") {
onProgressToast(opts.app, {
message: value.message,
progress: value.progress ?? null,
});
} else if (value.type === "done") {
return;
}
}
// Generator completed without explicit "done" message
return;
}
if (onExportResult instanceof Promise) {
onProgressToast(opts.app, { progress: null });
await onExportResult;
}
}
function prepareDataForJSONExport(
elements: readonly ExcalidrawElement[],
appState: AppState,
files: BinaryFiles,
app: AppClassProperties,
): { abortController: AbortController; data: Promise<JSONExportData> } {
const abortController = new AbortController();
const signal = abortController.signal;
const dataPromise = new Promise<JSONExportData>(async (resolve) => {
try {
if (app.props.onExport) {
await handleOnExportResult(
app.props.onExport(
"json",
{
elements,
appState,
files,
},
{
signal,
},
),
{
app,
signal,
},
);
}
} catch (error: any) {
if (error?.name === "AbortError") {
// if abort error, assume it's a reaction on the signal being aborted
console.warn(
`onExport() aborted by host app (signal aborted: ${signal.aborted})`,
);
} else {
// non-abort error
//
console.error("Error during props.onExport() handling", error);
}
// either way, we currently don't allow host apps to cancel save actions
// so we resolve to orig data
}
resolve({
elements,
appState,
// return latest files in case they finished loading during onExport
files: app.files,
});
});
return {
abortController,
data: dataPromise,
};
}
// ---------------------------------------------------------------------------
// Save actions
// ---------------------------------------------------------------------------
export const actionSaveToActiveFile = register({
name: "saveToActiveFile",
label: "buttons.save",
@@ -163,42 +310,62 @@ export const actionSaveToActiveFile = register({
);
},
perform: async (elements, appState, value, app) => {
const fileHandleExists = !!appState.fileHandle;
if (onExportInProgress) {
return false;
}
onExportInProgress = true;
const previousFileHandle = appState.fileHandle;
const filename = app.getName();
const { abortController, data: exportedDataPromise } =
prepareDataForJSONExport(elements, appState, app.files, app);
try {
const { fileHandle } = isImageFileHandle(appState.fileHandle)
const { fileHandle } = isImageFileHandle(previousFileHandle)
? await resaveAsImageWithScene(
elements,
appState,
app.files,
app.getName(),
exportedDataPromise,
previousFileHandle,
filename,
)
: await saveAsJSON(elements, appState, app.files, app.getName());
: await saveAsJSON({
data: exportedDataPromise,
filename,
fileHandle: previousFileHandle,
});
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
captureUpdate: CaptureUpdateAction.NEVER,
appState: {
...appState,
fileHandle,
toast: fileHandleExists
? {
message: fileHandle?.name
? t("toast.fileSavedToFilename").replace(
"{filename}",
`"${fileHandle.name}"`,
)
: t("toast.fileSaved"),
}
: null,
toast: {
message:
previousFileHandle && fileHandle?.name
? t("toast.fileSavedToFilename").replace(
"{filename}",
`"${fileHandle.name}"`,
)
: t("toast.fileSaved"),
duration: 1500,
},
},
};
} catch (error: any) {
abortController.abort();
if (error?.name !== "AbortError") {
console.error(error);
} else {
console.warn(error);
}
return { captureUpdate: CaptureUpdateAction.EVENTUALLY };
return {
captureUpdate: CaptureUpdateAction.NEVER,
appState: {
toast: null,
},
};
} finally {
onExportInProgress = false;
}
},
keyTest: (event) =>
@@ -212,36 +379,50 @@ export const actionSaveFileToDisk = register({
viewMode: true,
trackEvent: { category: "export" },
perform: async (elements, appState, value, app) => {
if (onExportInProgress) {
return false;
}
onExportInProgress = true;
const { abortController, data: exportedDataPromise } =
prepareDataForJSONExport(elements, appState, app.files, app);
try {
const { fileHandle } = await saveAsJSON(
elements,
{
...appState,
fileHandle: null,
},
app.files,
app.getName(),
);
const { fileHandle: savedFileHandle } = await saveAsJSON({
data: exportedDataPromise,
filename: app.getName(),
fileHandle: null,
});
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
captureUpdate: CaptureUpdateAction.NEVER,
appState: {
...appState,
openDialog: null,
fileHandle,
fileHandle: savedFileHandle,
toast: { message: t("toast.fileSaved") },
},
};
} catch (error: any) {
abortController.abort();
if (error?.name !== "AbortError") {
console.error(error);
} else {
console.warn(error);
}
return { captureUpdate: CaptureUpdateAction.EVENTUALLY };
return {
captureUpdate: CaptureUpdateAction.NEVER,
appState: {
toast: null,
},
};
} finally {
onExportInProgress = false;
}
},
keyTest: (event) =>
event.key === KEYS.S && event.shiftKey && event[KEYS.CTRL_OR_CMD],
event.key.toLowerCase() === KEYS.S &&
event.shiftKey &&
event[KEYS.CTRL_OR_CMD],
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
@@ -300,7 +481,8 @@ export const actionExportWithDarkMode = register<
name: "exportWithDarkMode",
label: "imageExportDialog.label.darkMode",
trackEvent: { category: "export", action: "toggleTheme" },
perform: (_elements, appState, value) => {
perform: (_elements, appState, value, app) => {
app.sessionExportThemeOverride = value ? THEME.DARK : THEME.LIGHT;
return {
appState: { ...appState, exportWithDarkMode: value },
captureUpdate: CaptureUpdateAction.EVENTUALLY,
@@ -18,7 +18,7 @@ import { HistoryChangedEvent } from "../history";
import { useEmitter } from "../hooks/useEmitter";
import { t } from "../i18n";
import { useStylesPanelMode } from "..";
import { useStylesPanelMode } from "../components/App";
import type { History } from "../history";
import type { AppClassProperties, AppState } from "../types";
+2 -4
View File
@@ -33,8 +33,6 @@ import {
normalizeFile,
} from "./data/blob";
import type { FileSystemHandle } from "./data/filesystem";
import type { BinaryFiles } from "./types";
type ElementsClipboard = {
@@ -369,7 +367,7 @@ type AllowedParsedDataTransferItem =
type: ValueOf<typeof IMAGE_MIME_TYPES>;
kind: "file";
file: File;
fileHandle: FileSystemHandle | null;
fileHandle: FileSystemFileHandle | null;
}
| { type: ValueOf<typeof STRING_MIME_TYPES>; kind: "string"; value: string };
@@ -378,7 +376,7 @@ type ParsedDataTransferItem =
type: string;
kind: "file";
file: File;
fileHandle: FileSystemHandle | null;
fileHandle: FileSystemFileHandle | null;
}
| { type: string; kind: "string"; value: string };
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,208 @@
import type { AppState, UnsubscribeCallback } from "../types";
type StateChangeSelector =
| keyof AppState
| (keyof AppState)[]
| ((appState: AppState) => unknown);
type StateChangePredicateOptions = {
predicate: (appState: AppState) => boolean;
callback?: (appState: AppState) => void;
once?: boolean;
};
type StateChangeArg = StateChangeSelector | StateChangePredicateOptions;
type StateChangeListener = {
predicate: (appState: AppState, prevState: AppState) => boolean;
getValue: (appState: AppState) => unknown;
callback: (value: any, appState: AppState) => void;
once: boolean;
};
type NormalizedStateChange = {
predicate: StateChangeListener["predicate"];
getValue: StateChangeListener["getValue"];
callback?: StateChangeListener["callback"];
once: boolean;
matchesImmediately: boolean;
};
export type OnStateChange = {
<K extends keyof AppState>(
prop: K,
callback: (value: AppState[K], appState: AppState) => void,
opts?: { once: boolean },
): UnsubscribeCallback;
<K extends keyof AppState>(prop: K): Promise<AppState[K]>;
(
prop: (keyof AppState)[],
callback: (appState: AppState, appState2: AppState) => void,
opts?: { once: boolean },
): UnsubscribeCallback;
(prop: (keyof AppState)[]): Promise<AppState>;
<T>(
prop: (appState: AppState) => T,
callback: (value: T, appState: AppState) => void,
opts?: { once: boolean },
): UnsubscribeCallback;
<T>(prop: (appState: AppState) => T): Promise<T>;
(opts: {
predicate: (appState: AppState) => boolean;
callback: (appState: AppState) => void;
once?: boolean;
}): UnsubscribeCallback;
(opts: { predicate: (appState: AppState) => boolean }): Promise<AppState>;
(
selector: StateChangeSelector,
callback: (value: any, appState: AppState) => void,
): any;
};
export class AppStateObserver {
private listeners: StateChangeListener[] = [];
constructor(private readonly getState: () => AppState) {}
private isStateChangePredicateOptions(
propOrOpts: StateChangeArg,
): propOrOpts is StateChangePredicateOptions {
return (
typeof propOrOpts === "object" &&
!Array.isArray(propOrOpts) &&
"predicate" in propOrOpts
);
}
private subscribe(listener: StateChangeListener): UnsubscribeCallback {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(
(existingListener) => existingListener !== listener,
);
};
}
private normalize(
propOrOpts: StateChangeArg,
callback?: (value: any, appState: AppState) => void,
opts?: { once: boolean },
): NormalizedStateChange {
let predicate: StateChangeListener["predicate"];
let getValue: StateChangeListener["getValue"];
let normalizedCallback = callback;
let once = opts?.once ?? false;
let matchesImmediately = false;
if (this.isStateChangePredicateOptions(propOrOpts)) {
const {
predicate: predicateFn,
callback: callbackFromOpts,
once: onceFromOpts,
} = propOrOpts;
predicate = predicateFn;
getValue = (appState: AppState) => appState;
normalizedCallback = callbackFromOpts
? (_value: AppState, appState: AppState) => callbackFromOpts(appState)
: undefined;
once = onceFromOpts ?? false;
matchesImmediately = predicateFn(this.getState());
} else if (typeof propOrOpts === "function") {
const selector = propOrOpts;
predicate = (appState: AppState, prevState: AppState) =>
selector(appState) !== selector(prevState);
getValue = (appState: AppState) => selector(appState);
} else if (Array.isArray(propOrOpts)) {
const keys = propOrOpts;
predicate = (appState: AppState, prevState: AppState) =>
keys.some((key) => appState[key] !== prevState[key]);
getValue = (appState: AppState) => appState;
} else {
const key = propOrOpts;
predicate = (appState: AppState, prevState: AppState) =>
appState[key] !== prevState[key];
getValue = (appState: AppState) => appState[key];
}
return {
predicate,
getValue,
callback: normalizedCallback,
once,
matchesImmediately,
};
}
public onStateChange: OnStateChange = ((
propOrOpts: StateChangeArg,
callback?: any,
opts?: { once: boolean },
) => {
const {
predicate,
getValue,
callback: stateChangeCallback,
once,
matchesImmediately,
} = this.normalize(propOrOpts, callback, opts);
if (stateChangeCallback) {
if (matchesImmediately) {
queueMicrotask(() => {
const state = this.getState();
stateChangeCallback(getValue(state), state);
});
if (once) {
return () => {};
}
}
return this.subscribe({
predicate,
getValue,
callback: stateChangeCallback,
once,
});
}
if (matchesImmediately) {
return Promise.resolve(getValue(this.getState()));
}
return new Promise<any>((resolve) => {
this.subscribe({
predicate,
getValue,
callback: (value) => resolve(value),
once: true,
});
});
}) as OnStateChange;
public flush(prevState: AppState) {
if (!this.listeners.length) {
return;
}
const state = this.getState();
const listenersToKeep: StateChangeListener[] = [];
for (const listener of this.listeners) {
if (listener.predicate(state, prevState)) {
listener.callback(listener.getValue(state), state);
if (!listener.once) {
listenersToKeep.push(listener);
}
} else {
listenersToKeep.push(listener);
}
}
this.listeners = listenersToKeep;
}
public clear() {
this.listeners = [];
}
}
@@ -10,12 +10,11 @@ import {
isWritableElement,
} from "@excalidraw/common";
import { actionToggleShapeSwitch } from "@excalidraw/excalidraw/actions/actionToggleShapeSwitch";
import { getShortcutKey } from "@excalidraw/excalidraw/shortcut";
import type { MarkRequired } from "@excalidraw/common/utility-types";
import { actionToggleShapeSwitch } from "../../actions/actionToggleShapeSwitch";
import { getShortcutKey } from "../../shortcut";
import {
actionClearCanvas,
actionLink,
@@ -1,7 +1,9 @@
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
import {
bumpVersion,
getLinearElementSubType,
mutateElement,
updateElbowArrowPoints,
} from "@excalidraw/element";
@@ -37,6 +39,8 @@ import {
isProdEnv,
mapFind,
reduceToCommonValue,
ROUNDNESS,
sceneCoordsToViewportCoords,
updateActiveTool,
} from "@excalidraw/common";
@@ -71,12 +75,6 @@ import type {
import type { Scene } from "@excalidraw/element";
import {
bumpVersion,
mutateElement,
ROUNDNESS,
sceneCoordsToViewportCoords,
} from "..";
import { trackEvent } from "../analytics";
import { atom } from "../editor-jotai";
@@ -59,6 +59,7 @@ type ImageExportModalProps = {
actionManager: ActionManager;
onExportImage: AppClassProperties["onExportImage"];
name: string;
exportWithDarkMode: boolean;
};
const ImageExportModal = ({
@@ -68,6 +69,7 @@ const ImageExportModal = ({
actionManager,
onExportImage,
name,
exportWithDarkMode,
}: ImageExportModalProps) => {
const hasSelection = isSomeElementSelected(
elementsSnapshot,
@@ -79,15 +81,13 @@ const ImageExportModal = ({
const [exportWithBackground, setExportWithBackground] = useState(
appStateSnapshot.exportBackground,
);
const [exportDarkMode, setExportDarkMode] = useState(
appStateSnapshot.exportWithDarkMode,
);
const [embedScene, setEmbedScene] = useState(
appStateSnapshot.exportEmbedScene,
);
const [exportScale, setExportScale] = useState(appStateSnapshot.exportScale);
const previewRef = useRef<HTMLDivElement>(null);
const previewRenderRequestIdRef = useRef(0);
const [renderError, setRenderError] = useState<Error | null>(null);
const { onCopy, copyStatus, resetCopyStatus } = useCopyStatus();
@@ -99,7 +99,7 @@ const ImageExportModal = ({
}, [
projectName,
exportWithBackground,
exportDarkMode,
exportWithDarkMode,
exportScale,
embedScene,
resetCopyStatus,
@@ -122,13 +122,18 @@ const ImageExportModal = ({
return;
}
const requestId = ++previewRenderRequestIdRef.current;
const isStaleRequest = () => {
return requestId !== previewRenderRequestIdRef.current;
};
exportToCanvas({
elements: exportedElements,
appState: {
...appStateSnapshot,
name: projectName,
exportBackground: exportWithBackground,
exportWithDarkMode: exportDarkMode,
exportWithDarkMode,
exportScale,
exportEmbedScene: embedScene,
},
@@ -137,25 +142,41 @@ const ImageExportModal = ({
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
exportingFrame,
})
.then((canvas) => {
.then(async (canvas) => {
if (isStaleRequest()) {
return;
}
// If converting to blob fails, there's some problem that will likely
// prevent preview and export (e.g. canvas too big).
try {
await canvasToBlob(canvas);
} catch (error: any) {
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
throw new Error(t("canvasError.canvasTooBig"));
}
throw error;
}
if (isStaleRequest()) {
return;
}
setRenderError(null);
// if converting to blob fails, there's some problem that will
// likely prevent preview and export (e.g. canvas too big)
return canvasToBlob(canvas)
.then(() => {
previewNode.replaceChildren(canvas);
})
.catch((e) => {
if (e.name === "CANVAS_POSSIBLY_TOO_BIG") {
throw new Error(t("canvasError.canvasTooBig"));
}
throw e;
});
previewNode.replaceChildren(canvas);
})
.catch((error) => {
if (isStaleRequest()) {
return;
}
console.error(error);
setRenderError(error);
});
return () => {
previewRenderRequestIdRef.current += 1;
};
}, [
appStateSnapshot,
files,
@@ -163,7 +184,7 @@ const ImageExportModal = ({
exportingFrame,
projectName,
exportWithBackground,
exportDarkMode,
exportWithDarkMode,
exportScale,
embedScene,
]);
@@ -233,9 +254,8 @@ const ImageExportModal = ({
>
<Switch
name="exportDarkModeSwitch"
checked={exportDarkMode}
checked={exportWithDarkMode}
onChange={(checked) => {
setExportDarkMode(checked);
actionManager.executeAction(
actionExportWithDarkMode,
"ui",
@@ -399,6 +419,7 @@ export const ImageExportDialog = ({
actionManager={actionManager}
onExportImage={onExportImage}
name={name}
exportWithDarkMode={appState.exportWithDarkMode}
/>
</Dialog>
);
+25 -12
View File
@@ -60,6 +60,7 @@ import { ImageExportDialog } from "./ImageExportDialog";
import { Island } from "./Island";
import { JSONExportDialog } from "./JSONExportDialog";
import { LaserPointerButton } from "./LaserPointerButton";
import { Toast } from "./Toast";
import "./LayerUI.scss";
import "./Toolbar.scss";
@@ -605,18 +606,30 @@ const LayerUI = ({
showExitZenModeBtn={showExitZenModeBtn}
renderWelcomeScreen={renderWelcomeScreen}
/>
{appState.scrolledOutside && (
<button
type="button"
className="scroll-back-to-content"
onClick={() => {
setAppState((appState) => ({
...calculateScrollCenter(elements, appState),
}));
}}
>
{t("buttons.scrollBackToContent")}
</button>
{(appState.toast || appState.scrolledOutside) && (
<div className="floating-status-stack">
{appState.toast && (
<Toast
message={appState.toast.message}
onClose={() => setAppState({ toast: null })}
duration={appState.toast.duration}
closable={appState.toast.closable}
/>
)}
{!appState.toast && appState.scrolledOutside && (
<button
type="button"
className="scroll-back-to-content"
onClick={() => {
setAppState((appState) => ({
...calculateScrollCenter(elements, appState),
}));
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</div>
)}
</div>
{renderSidebars()}
@@ -472,9 +472,9 @@ export const MobileToolBar = ({
onSelect={() => app.onMagicframeToolSelect()}
icon={MagicIcon}
data-testid="toolbar-magicframe"
badge={<DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>}
>
{t("toolBar.magicframe")}
<DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>
</DropdownMenu.Item>
</>
)}
@@ -11,7 +11,7 @@ import { rateLimitsAtom } from "../TTDContext";
import { ChatHistoryMenu } from "./ChatHistoryMenu";
import { ChatInterface } from ".";
import { ChatInterface } from "./ChatInterface";
import type { TTDPanelAction } from "../TTDDialogPanel";
@@ -0,0 +1,239 @@
import { useEffect, useRef } from "react";
import {
Decoration,
EditorView,
keymap,
lineNumbers,
placeholder as cmPlaceholder,
drawSelection,
} from "@codemirror/view";
import { Compartment, EditorState, type Extension } from "@codemirror/state";
import {
defaultKeymap,
history,
historyKeymap,
redo,
} from "@codemirror/commands";
import { syntaxHighlighting, HighlightStyle } from "@codemirror/language";
import { tags } from "@lezer/highlight";
import type { Theme } from "@excalidraw/element/types";
import { mermaidLite } from "./mermaid-lang-lite";
export interface CodeMirrorEditorProps {
value: string;
onChange: (value: string) => void;
onKeyboardSubmit?: () => void;
placeholder?: string;
theme: Theme;
errorLine?: number | null;
}
// ---- Dark theme ----
const darkTheme = EditorView.theme(
{
"&": {
backgroundColor: "#1e1e1e",
color: "#d4d4d4",
},
".cm-content": { caretColor: "#fff" },
".cm-cursor": { borderLeftColor: "#fff" },
".cm-gutters": {
backgroundColor: "#1e1e1e",
color: "#858585",
border: "none",
},
".cm-activeLineGutter": { backgroundColor: "#2a2a2a" },
".cm-activeLine": { backgroundColor: "#2a2a2a" },
".cm-errorLine": { backgroundColor: "rgba(255, 0, 0, 0.15)" },
},
{ dark: true },
);
const darkHighlight = HighlightStyle.define([
{ tag: tags.keyword, color: "#569cd6" },
{ tag: tags.string, color: "#ce9178" },
{ tag: tags.comment, color: "#6a9955" },
{ tag: tags.number, color: "#b5cea8" },
{ tag: tags.operator, color: "#d4d4d4" },
{ tag: tags.punctuation, color: "#d4d4d4" },
{ tag: tags.variableName, color: "#9cdcfe" },
{ tag: tags.bracket, color: "#ffd700" },
]);
// ---- Light theme ----
const lightTheme = EditorView.theme({
"&": {
backgroundColor: "#ffffff",
color: "#1e1e1e",
},
".cm-content": { caretColor: "#000" },
".cm-cursor": { borderLeftColor: "#000" },
".cm-gutters": {
backgroundColor: "#fff",
color: "#999",
border: "none",
},
".cm-activeLineGutter": { backgroundColor: "#e8e8e8" },
".cm-activeLine": { backgroundColor: "#e8e8e8" },
".cm-errorLine": { backgroundColor: "rgba(255, 0, 0, 0.1)" },
});
const lightHighlight = HighlightStyle.define([
{ tag: tags.keyword, color: "#0000ff" },
{ tag: tags.string, color: "#a31515" },
{ tag: tags.comment, color: "#008000" },
{ tag: tags.number, color: "#098658" },
{ tag: tags.operator, color: "#1e1e1e" },
{ tag: tags.punctuation, color: "#1e1e1e" },
{ tag: tags.variableName, color: "#001080" },
{ tag: tags.bracket, color: "#af00db" },
]);
// ---- Error line decoration ----
const errorLineDeco = Decoration.line({ class: "cm-errorLine" });
const getErrorLineExtension = (
errorLine: number | null | undefined,
doc: { line(n: number): { from: number }; lines: number },
): Extension => {
if (!errorLine || errorLine < 1 || errorLine > doc.lines) {
return EditorView.decorations.of(Decoration.none);
}
const line = doc.line(errorLine);
return EditorView.decorations.of(
Decoration.set([errorLineDeco.range(line.from)]),
);
};
// ---- Helpers ----
const getThemeExtensions = (theme: Theme) => {
if (theme === "dark") {
return [darkTheme, syntaxHighlighting(darkHighlight)];
}
return [lightTheme, syntaxHighlighting(lightHighlight)];
};
const CodeMirrorEditor = ({
value,
onChange,
onKeyboardSubmit,
placeholder,
theme,
errorLine,
}: CodeMirrorEditorProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
const onChangeRef = useRef(onChange);
const onKeyboardSubmitRef = useRef(onKeyboardSubmit);
const themeCompartmentRef = useRef(new Compartment());
const errorLineCompartmentRef = useRef(new Compartment());
onChangeRef.current = onChange;
onKeyboardSubmitRef.current = onKeyboardSubmit;
useEffect(() => {
if (!containerRef.current) {
return;
}
const themeCompartment = themeCompartmentRef.current;
const view = new EditorView({
state: EditorState.create({
doc: value,
extensions: [
keymap.of([
{
key: "Mod-Enter",
run: () => {
onKeyboardSubmitRef.current?.();
return true;
},
},
// historyKeymap binds Mod-Shift-z only on Mac; add it for all platforms
{ key: "Mod-Shift-z", run: redo, preventDefault: true },
]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
onChangeRef.current(update.state.doc.toString());
}
}),
history(),
keymap.of([...defaultKeymap, ...historyKeymap]),
lineNumbers(),
EditorView.lineWrapping,
themeCompartment.of(getThemeExtensions(theme)),
errorLineCompartmentRef.current.of([]),
mermaidLite(),
drawSelection({ drawRangeCursor: true }),
...(placeholder ? [cmPlaceholder(placeholder)] : []),
],
}),
parent: containerRef.current,
});
viewRef.current = view;
view.focus();
return () => {
view.destroy();
viewRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Swap theme dynamically via compartment
useEffect(() => {
const view = viewRef.current;
if (!view) {
return;
}
view.dispatch({
effects: themeCompartmentRef.current.reconfigure(
getThemeExtensions(theme),
),
});
}, [theme]);
// Update error line highlight
useEffect(() => {
const view = viewRef.current;
if (!view) {
return;
}
view.dispatch({
effects: errorLineCompartmentRef.current.reconfigure(
getErrorLineExtension(errorLine, view.state.doc),
),
});
}, [errorLine]);
// Sync external value changes into EditorView
useEffect(() => {
const view = viewRef.current;
if (!view) {
return;
}
const currentDoc = view.state.doc.toString();
if (value !== currentDoc) {
view.dispatch({
changes: { from: 0, to: currentDoc.length, insert: value },
});
}
}, [value]);
return (
<div
ref={containerRef}
className="ttd-dialog-input ttd-dialog-input--codemirror"
/>
);
};
export default CodeMirrorEditor;
@@ -17,6 +17,11 @@ import { TTDDialogOutput } from "./TTDDialogOutput";
import { TTDDialogPanel } from "./TTDDialogPanel";
import { TTDDialogPanels } from "./TTDDialogPanels";
import { TTDDialogSubmitShortcut } from "./TTDDialogSubmitShortcut";
import {
getMermaidErrorLineNumber,
isMermaidAutoFixableError,
} from "./utils/mermaidError";
import { getMermaidAutoFixCandidates } from "./utils/mermaidAutoFix";
import {
convertMermaidToExcalidraw,
insertToEditor,
@@ -33,6 +38,27 @@ const MERMAID_EXAMPLE =
"flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]";
const debouncedSaveMermaidDefinition = debounce(saveMermaidDataToStorage, 300);
const AUTO_FIX_DEBOUNCE_MS = 500;
const AUTO_FIX_MAX_DEPTH = 4;
const AUTO_FIX_MAX_CANDIDATES = 30;
const getErrorMessage = (error: unknown): string => {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "string") {
return error;
}
if (
error &&
typeof error === "object" &&
"message" in error &&
typeof (error as { message?: unknown }).message === "string"
) {
return (error as { message: string }).message;
}
return "";
};
const MermaidToExcalidraw = ({
mermaidToExcalidrawLib,
@@ -46,8 +72,16 @@ const MermaidToExcalidraw = ({
EditorLocalStorage.get<string>(EDITOR_LS_KEYS.MERMAID_TO_EXCALIDRAW) ||
MERMAID_EXAMPLE,
);
const deferredText = useDeferredValue(text.trim());
const deferredText = useDeferredValue(text);
const [error, setError] = useState<Error | null>(null);
const [autoFixCandidate, setAutoFixCandidate] = useState<string | null>(null);
const errorLine = (() => {
if (!error?.message) {
return null;
}
return getMermaidErrorLineNumber(error.message, deferredText);
})();
const canvasRef = useRef<HTMLDivElement>(null);
const data = useRef<{
@@ -61,7 +95,7 @@ const MermaidToExcalidraw = ({
useEffect(() => {
const doRender = async () => {
try {
if (!deferredText) {
if (!deferredText.trim()) {
resetPreview({ canvasRef, setError });
return;
}
@@ -98,6 +132,88 @@ const MermaidToExcalidraw = ({
[],
);
useEffect(() => {
const errorMessage = error?.message ?? "";
const sourceText = deferredText;
const shouldTryAutoFix =
isActive &&
isMermaidAutoFixableError(errorMessage) &&
!!sourceText.trim() &&
mermaidToExcalidrawLib.loaded;
if (!shouldTryAutoFix) {
setAutoFixCandidate(null);
return;
}
const candidates = getMermaidAutoFixCandidates(sourceText, errorMessage);
if (!candidates.length) {
setAutoFixCandidate(null);
return;
}
let cancelled = false;
const timer = setTimeout(async () => {
try {
const api = await mermaidToExcalidrawLib.api;
const seen = new Set<string>([sourceText]);
const queue = candidates.map((candidate) => ({
text: candidate,
depth: 1,
}));
let triedCandidates = 0;
while (queue.length > 0 && triedCandidates < AUTO_FIX_MAX_CANDIDATES) {
const current = queue.shift();
if (!current || seen.has(current.text)) {
continue;
}
seen.add(current.text);
triedCandidates += 1;
try {
await api.parseMermaidToExcalidraw(current.text);
if (!cancelled) {
setAutoFixCandidate(current.text);
}
return;
} catch (candidateError) {
if (current.depth >= AUTO_FIX_MAX_DEPTH) {
continue;
}
const nextErrorMessage = getErrorMessage(candidateError);
if (!nextErrorMessage) {
continue;
}
const nextCandidates = getMermaidAutoFixCandidates(
current.text,
nextErrorMessage,
);
for (const nextCandidate of nextCandidates) {
if (!seen.has(nextCandidate)) {
queue.push({
text: nextCandidate,
depth: current.depth + 1,
});
}
}
}
}
} catch {
// ignore auto-fix probe errors
}
if (!cancelled) {
setAutoFixCandidate(null);
}
}, AUTO_FIX_DEBOUNCE_MS);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [deferredText, error?.message, isActive, mermaidToExcalidrawLib]);
const onInsertToEditor = () => {
insertToEditor({
app,
@@ -107,6 +223,13 @@ const MermaidToExcalidraw = ({
});
};
const onApplyAutoFix = () => {
if (!autoFixCandidate) {
return;
}
setText(autoFixCandidate);
};
return (
<>
<div className="ttd-dialog-desc">
@@ -130,7 +253,8 @@ const MermaidToExcalidraw = ({
<TTDDialogInput
input={text}
placeholder={t("mermaid.inputPlaceholder")}
onChange={(event) => setText(event.target.value)}
onChange={(value) => setText(value)}
errorLine={errorLine}
onKeyboardSubmit={() => {
onInsertToEditor();
}}
@@ -153,6 +277,9 @@ const MermaidToExcalidraw = ({
canvasRef={canvasRef}
loaded={mermaidToExcalidrawLib.loaded}
error={error}
sourceText={text}
autoFixAvailable={!!autoFixCandidate}
onApplyAutoFix={onApplyAutoFix}
/>
</TTDDialogPanel>
</TTDDialogPanels>
@@ -219,6 +219,49 @@ $fullScreenModalBreakpoint: 600px;
}
}
.ttd-dialog-input--loading {
display: flex;
align-items: center;
justify-content: center;
}
.ttd-dialog-input--codemirror {
padding: 0;
overflow: hidden;
// Override height:100% from .ttd-dialog-input — use flex sizing
// so the editor fills remaining space without overflowing the panel
height: 0;
flex: 1 1 0;
min-height: 0;
.cm-editor {
height: 100%;
font-family: monospace;
&.cm-focused {
outline: none;
}
}
.cm-scroller {
padding: 0.85rem 0;
overflow: auto;
}
.cm-gutters {
padding-left: 0.25rem;
}
.cm-content {
padding: 0;
}
.cm-placeholder {
color: var(--color-gray-40);
font-style: italic;
}
}
.ttd-dialog-output-wrapper {
display: flex;
flex-direction: column;
@@ -331,14 +374,55 @@ $fullScreenModalBreakpoint: 600px;
margin-top: 0.25rem;
}
.ttd-dialog-output-error-summary {
width: 100%;
max-width: 640px;
color: var(--color-gray-50);
font-size: 0.9rem;
text-align: left;
&__headline {
font-weight: 600;
color: var(--color-gray-60);
}
&__label {
margin-top: 0.35rem;
font-weight: 500;
}
&__causes {
margin: 0.35rem 0 0;
padding-left: 2rem;
}
}
.ttd-dialog-output-error-message {
text-align: left;
font-weight: 400;
color: var(--color-gray-50);
word-break: break-word;
white-space: pre-wrap;
max-width: 100%;
max-width: 640px;
width: 100%;
font-family: monospace;
&__caret {
color: var(--color-danger);
}
}
.ttd-dialog-output-error-autofix-slot {
align-self: flex-start;
margin-top: 0.35rem;
min-height: 2.5rem;
display: flex;
align-items: flex-start;
}
.ttd-dialog-output-error-autofix {
margin-top: 0;
white-space: nowrap;
}
}
@@ -1,28 +1,84 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { EVENT, KEYS } from "@excalidraw/common";
import type { ChangeEventHandler } from "react";
import Spinner from "../Spinner";
import { useUIAppState } from "../../context/ui-appState";
import type { ComponentType } from "react";
import type { CodeMirrorEditorProps } from "./CodeMirrorEditor";
interface TTDDialogInputProps {
input: string;
placeholder: string;
onChange: ChangeEventHandler<HTMLTextAreaElement>;
onChange: (value: string) => void;
onKeyboardSubmit?: () => void;
errorLine?: number | null;
}
type EditorState =
| { type: "loading" }
| { type: "ready"; component: ComponentType<CodeMirrorEditorProps> }
| { type: "fallback" };
const SPINNER_DELAY_MS = 300;
export const TTDDialogInput = ({
input,
placeholder,
onChange,
onKeyboardSubmit,
errorLine,
}: TTDDialogInputProps) => {
const ref = useRef<HTMLTextAreaElement>(null);
const callbackRef = useRef(onKeyboardSubmit);
callbackRef.current = onKeyboardSubmit;
const [editorState, setEditorState] = useState<EditorState>({
type: "loading",
});
const [showSpinner, setShowSpinner] = useState(false);
const { theme } = useUIAppState();
// Lazy-load CodeMirror editor
useEffect(() => {
let cancelled = false;
const spinnerTimer = setTimeout(() => {
if (!cancelled) {
setShowSpinner(true);
}
}, SPINNER_DELAY_MS);
import("./CodeMirrorEditor")
.then((mod) => {
if (!cancelled) {
setEditorState({ type: "ready", component: mod.default });
}
})
.catch(() => {
if (!cancelled) {
setEditorState({ type: "fallback" });
}
})
.finally(() => {
clearTimeout(spinnerTimer);
});
return () => {
cancelled = true;
clearTimeout(spinnerTimer);
};
}, []);
// Keyboard shortcut + focus for textarea fallback
useEffect(() => {
if (editorState.type !== "fallback") {
return;
}
if (!callbackRef.current) {
return;
}
@@ -40,15 +96,42 @@ export const TTDDialogInput = ({
textarea.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
};
}
}, []);
}, [editorState.type]);
return (
<textarea
className="ttd-dialog-input"
onChange={onChange}
value={input}
placeholder={placeholder}
ref={ref}
/>
);
if (editorState.type === "ready") {
const CodeMirrorEditor = editorState.component;
return (
<CodeMirrorEditor
value={input}
onChange={onChange}
onKeyboardSubmit={onKeyboardSubmit}
placeholder={placeholder}
theme={theme}
errorLine={errorLine}
/>
);
}
if (editorState.type === "fallback") {
return (
<textarea
className="ttd-dialog-input"
onChange={(e) => onChange(e.target.value)}
value={input}
placeholder={placeholder}
ref={ref}
/>
);
}
// Loading state
if (showSpinner) {
return (
<div className="ttd-dialog-input ttd-dialog-input--loading">
<Spinner />
</div>
);
}
return null;
};
@@ -1,14 +1,24 @@
import clsx from "clsx";
import { Button } from "../Button";
import Spinner from "../Spinner";
import { t } from "../../i18n";
import { alertTriangleIcon } from "../icons";
import {
formatMermaidParseErrorMessage,
getMermaidSyntaxErrorGuidance,
isMermaidCaretLine,
} from "./utils/mermaidError";
interface TTDDialogOutputProps {
error: Error | null;
canvasRef: React.RefObject<HTMLDivElement | null>;
loaded: boolean;
hideErrorDetails?: boolean;
sourceText?: string;
autoFixAvailable?: boolean;
onApplyAutoFix?: () => void;
}
export const TTDDialogOutput = ({
@@ -16,7 +26,24 @@ export const TTDDialogOutput = ({
canvasRef,
loaded,
hideErrorDetails,
sourceText,
autoFixAvailable,
onApplyAutoFix,
}: TTDDialogOutputProps) => {
const errorMessage = error
? hideErrorDetails
? t("chat.errors.mermaidParseError")
: formatMermaidParseErrorMessage(error.message)
: null;
const syntaxGuidance =
error && !hideErrorDetails
? getMermaidSyntaxErrorGuidance(error.message, sourceText)
: null;
const showAutoFixButton =
!!autoFixAvailable && !!onApplyAutoFix && !hideErrorDetails;
const errorMessageLines = errorMessage?.split(/\r?\n/) ?? [];
return (
<div
className={`ttd-dialog-output-wrapper ${
@@ -33,14 +60,48 @@ export const TTDDialogOutput = ({
<div className="ttd-dialog-output-error-icon">
{alertTriangleIcon}
</div>
<div className="ttd-dialog-output-error-title">
{t("ttd.error")}
</div>
{syntaxGuidance && (
<div className="ttd-dialog-output-error-summary">
<div className="ttd-dialog-output-error-summary__headline">
{syntaxGuidance.summary}
</div>
<div className="ttd-dialog-output-error-summary__label">
Likely causes:
</div>
<ul className="ttd-dialog-output-error-summary__causes">
{syntaxGuidance.likelyCauses.map((cause) => (
<li key={cause}>{cause}</li>
))}
</ul>
</div>
)}
<div className="ttd-dialog-output-error-message">
{hideErrorDetails
? t("chat.errors.mermaidParseError")
: error.message}
{errorMessageLines.map((line, index) => (
<span
key={`error-line-${index}`}
className={
isMermaidCaretLine(line)
? "ttd-dialog-output-error-message__caret"
: undefined
}
>
{line}
{index < errorMessageLines.length - 1 ? "\n" : ""}
</span>
))}
</div>
{!hideErrorDetails && (
<div className="ttd-dialog-output-error-autofix-slot">
{showAutoFixButton ? (
<Button
className="ttd-dialog-panel-button ttd-dialog-output-error-autofix"
onSelect={onApplyAutoFix}
>
{t("mermaid.autoFixAvailable")}
</Button>
) : null}
</div>
)}
</div>
</div>
)}
@@ -1,4 +1,4 @@
import { getShortcutKey } from "@excalidraw/excalidraw/shortcut";
import { getShortcutKey } from "../../shortcut";
export const TTDDialogSubmitShortcut = () => {
return (
@@ -0,0 +1,89 @@
import { describe, expect, it, vi } from "vitest";
import { convertMermaidToExcalidraw } from "./common";
type ConvertMermaidArgs = Parameters<typeof convertMermaidToExcalidraw>[0];
type ParseMermaidToExcalidraw = Awaited<
ConvertMermaidArgs["mermaidToExcalidrawLib"]["api"]
>["parseMermaidToExcalidraw"];
const createConvertArgs = (
mermaidDefinition: string,
parseMermaidToExcalidraw: ParseMermaidToExcalidraw,
): ConvertMermaidArgs => {
const parent = document.createElement("div");
const canvas = document.createElement("div");
parent.appendChild(canvas);
return {
canvasRef: { current: canvas },
mermaidToExcalidrawLib: {
loaded: true,
api: Promise.resolve({ parseMermaidToExcalidraw }),
},
mermaidDefinition,
setError: vi.fn(),
data: {
current: {
elements: [],
files: null,
},
},
theme: "light",
};
};
describe("convertMermaidToExcalidraw", () => {
it("returns the original parse error when quote-normalized fallback also fails", async () => {
const originalError = new Error("Parse error on line 9: ...");
const fallbackError = new Error("Parse error on line 6: ...");
const parseMermaidToExcalidraw = vi
.fn<ParseMermaidToExcalidraw>()
.mockRejectedValueOnce(originalError)
.mockRejectedValueOnce(fallbackError);
const mermaidDefinition =
'graph TD\nA["One"]\nB["Two"]x\nC["Three"]\nD["Four"]';
const result = await convertMermaidToExcalidraw(
createConvertArgs(mermaidDefinition, parseMermaidToExcalidraw),
);
expect(parseMermaidToExcalidraw).toHaveBeenCalledTimes(2);
expect(parseMermaidToExcalidraw).toHaveBeenNthCalledWith(
1,
mermaidDefinition,
);
expect(parseMermaidToExcalidraw).toHaveBeenNthCalledWith(
2,
mermaidDefinition.replace(/"/g, "'"),
);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBe(originalError);
}
});
it("does not retry quote normalization when the input has no double quotes", async () => {
const originalError = new Error("Parse error on line 9: ...");
const parseMermaidToExcalidraw = vi
.fn<ParseMermaidToExcalidraw>()
.mockRejectedValueOnce(originalError);
const mermaidDefinition = "graph TD\nA[One]\nB[Two]x";
const result = await convertMermaidToExcalidraw(
createConvertArgs(mermaidDefinition, parseMermaidToExcalidraw),
);
expect(parseMermaidToExcalidraw).toHaveBeenCalledTimes(1);
expect(parseMermaidToExcalidraw).toHaveBeenCalledWith(mermaidDefinition);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBe(originalError);
}
});
});
@@ -1,4 +1,12 @@
import { DEFAULT_EXPORT_PADDING, EDITOR_LS_KEYS } from "@excalidraw/common";
import {
DEFAULT_EXPORT_PADDING,
EDITOR_LS_KEYS,
THEME,
} from "@excalidraw/common";
import { convertToExcalidrawElements } from "@excalidraw/element";
import { exportToCanvas } from "@excalidraw/utils";
import type {
NonDeletedExcalidrawElement,
@@ -6,11 +14,6 @@ import type {
} from "@excalidraw/element/types";
import { EditorLocalStorage } from "../../data/EditorLocalStorage";
import {
convertToExcalidrawElements,
exportToCanvas,
THEME,
} from "../../index";
import type { MermaidToExcalidrawLibProps } from "./types";
@@ -72,15 +75,23 @@ export const convertMermaidToExcalidraw = async ({
const api = await mermaidToExcalidrawLib.api;
try {
ret = await api.parseMermaidToExcalidraw(mermaidDefinition);
} catch (err: unknown) {
const originalParseError = err as Error;
if (!mermaidDefinition.includes('"')) {
return { success: false, error: originalParseError };
}
try {
ret = await api.parseMermaidToExcalidraw(mermaidDefinition);
} catch (err: unknown) {
ret = await api.parseMermaidToExcalidraw(
mermaidDefinition.replace(/"/g, "'"),
);
} catch {
// Keep the original error so line/column references stay aligned with
// the user's unmodified input.
return { success: false, error: originalParseError };
}
} catch (err: unknown) {
return { success: false, error: err as Error };
}
const { elements, files } = ret;
@@ -0,0 +1,82 @@
import { StreamLanguage } from "@codemirror/language";
const mermaidStreamParser = StreamLanguage.define({
token(stream) {
// Comments: %%...
if (stream.match(/^%%.*$/)) {
return "comment";
}
// Strings
if (stream.match(/^"(?:[^"\\]|\\.)*"/)) {
return "string";
}
// Diagram type keywords (at start of line or after whitespace)
if (
stream.match(
/^(flowchart|graph|sequenceDiagram|classDiagram|stateDiagram|erDiagram|gantt|pie|mindmap|journey|gitGraph|timeline|quadrantChart|sankey|xychart)\b/i,
)
) {
return "keyword";
}
// Direction keywords
if (stream.match(/^(TB|TD|BT|RL|LR)\b/)) {
return "keyword";
}
// Keywords
if (
stream.match(
/^(subgraph|end|participant|actor|loop|alt|else|opt|par|critical|break|rect|note|over|activate|deactivate|title|section|class|style|linkStyle|classDef|click)\b/i,
)
) {
return "keyword";
}
// Arrows: -->, ---, -.->, ===>, etc.
if (stream.match(/^[-.=<>|ox]+>/)) {
return "operator";
}
if (stream.match(/^<[-.=<>|ox]+/)) {
return "operator";
}
if (stream.match(/^--+|\.\.+|==+/)) {
return "operator";
}
// Labels in brackets/parens: [text], (text), {text}, ((text)), etc.
if (stream.match(/^[[\](){}|<>]/)) {
return "bracket";
}
// Node IDs (alphanumeric)
if (stream.match(/^[A-Za-z_][A-Za-z0-9_]*/)) {
return "variableName";
}
// Numbers
if (stream.match(/^\d+(\.\d+)?/)) {
return "number";
}
// Punctuation
if (stream.match(/^[,:;]/)) {
return "punctuation";
}
// Skip whitespace
if (stream.eatSpace()) {
return null;
}
// Skip any other character
stream.next();
return null;
},
});
export function mermaidLite() {
return mermaidStreamParser;
}
@@ -1,9 +1,6 @@
import { RequestError } from "@excalidraw/excalidraw/errors";
import { RequestError } from "../../../errors";
import type {
LLMMessage,
TTTDDialog,
} from "@excalidraw/excalidraw/components/TTDDialog/types";
import type { LLMMessage, TTTDDialog } from "../types";
interface RateLimitInfo {
rateLimit?: number;
@@ -0,0 +1,116 @@
import { describe, expect, it } from "vitest";
import { getMermaidAutoFixCandidates } from "./mermaidAutoFix";
describe("getMermaidAutoFixCandidates", () => {
it("suggests removing trailing token after a closed label shape", () => {
const sourceText = `graph TD
L3_TCP["TCP (Transmission Control Protocol)"]x
L3_UDP["UDP (User Datagram Protocol)"]`;
const errorMessage = `Parse error on line 2:
...ission Control Protocol)"]x
-----------------------------^
Expecting 'SEMI', got 'NODE_STRING'`;
const candidates = getMermaidAutoFixCandidates(sourceText, errorMessage);
expect(candidates).toContain(`graph TD
L3_TCP["TCP (Transmission Control Protocol)"]
L3_UDP["UDP (User Datagram Protocol)"]`);
});
it("suggests appending missing end statements", () => {
const sourceText = `graph TD
subgraph A
A1[Start]`;
const errorMessage = `Parse error on line 3:
... A1[Start]
-------------^
Expecting 'end'`;
const candidates = getMermaidAutoFixCandidates(sourceText, errorMessage);
expect(candidates).toContain(`graph TD
subgraph A
A1[Start]
end`);
});
it("returns empty list for non-parse errors", () => {
expect(
getMermaidAutoFixCandidates("graph TD\nA-->B", "Network error"),
).toEqual([]);
});
it("extracts line index from lexical error format too", () => {
const sourceText = `graph TD
subgraph Layers["X"]x
direction TB`;
const errorMessage = `Lexical error on line 2. Unrecognized text.
... subgraph Layers["X"]x direction
-----------------------^`;
const candidates = getMermaidAutoFixCandidates(sourceText, errorMessage);
expect(candidates).toContain(`graph TD
subgraph Layers["X"]
direction TB`);
});
it("removes extra > after edge label", () => {
const sourceText = `flowchart TD
A["User Input"] -->|text|> B["Tokenization"]
A["User Input"] -->|text|> B["Tokenization"]`;
const errorMessage = `Parse error on line 2:
...A["User Input"] -->|text|> B["Tokenization"]
---------------------------^
Expecting 'NODE_STRING', got 'GT'`;
const candidates = getMermaidAutoFixCandidates(sourceText, errorMessage);
expect(candidates).toContain(`flowchart TD
A["User Input"] -->|text| B["Tokenization"]
A["User Input"] -->|text| B["Tokenization"]`);
});
it("suggests removing the last invalid deactivate for participant errors", () => {
const sourceText = `sequenceDiagram
participant QAEngineer as QA
activate QA
QA->>QA: Verifies Fix
deactivate QA
QA->>QA: Verifies Again
deactivate QA`;
const errorMessage = "Trying to inactivate an inactive participant (QA)";
const candidates = getMermaidAutoFixCandidates(sourceText, errorMessage);
expect(candidates).toContain(`sequenceDiagram
participant QAEngineer as QA
activate QA
QA->>QA: Verifies Fix
deactivate QA
QA->>QA: Verifies Again`);
});
it("adds a fallback candidate that removes all invalid deactivations", () => {
const sourceText = `sequenceDiagram
participant QAEngineer as QA
deactivate QA
QA->>QA: Verifies Fix
deactivate QA`;
const errorMessage = "Trying to inactivate an inactive participant (QA)";
const candidates = getMermaidAutoFixCandidates(sourceText, errorMessage);
expect(candidates).toContain(`sequenceDiagram
participant QAEngineer as QA
QA->>QA: Verifies Fix`);
});
});
@@ -0,0 +1,175 @@
import {
getMermaidErrorLineNumber,
getMermaidInactiveParticipant,
isMermaidAutoFixableError,
isMermaidParseSyntaxError,
} from "./mermaidError";
const getErrorLineIndex = (message: string, sourceText: string) => {
const lineNumber = getMermaidErrorLineNumber(message, sourceText);
if (lineNumber == null) {
return null;
}
return lineNumber - 1;
};
const replaceLineAt = (
lines: string[],
index: number,
transform: (line: string) => string,
) => {
if (index < 0 || index >= lines.length) {
return null;
}
const nextLine = transform(lines[index]);
if (nextLine === lines[index]) {
return null;
}
const nextLines = [...lines];
nextLines[index] = nextLine;
return nextLines.join("\n");
};
const stripTrailingTokenAfterShape = (line: string) => {
const alphaTailMatch = line.match(
/^(.*(?:\[[^\]]*]|\([^)]*\)|\{[^}]*}|"(?:[^"]*)"|'(?:[^']*)'))([A-Za-z]+)\s*$/,
);
if (alphaTailMatch) {
return alphaTailMatch[1];
}
const punctuationTailMatch = line.match(
/^(.*(?:\[[^\]]*]|\([^)]*\)|\{[^}]*}|"(?:[^"]*)"|'(?:[^']*)'))([,;:])\s*$/,
);
if (punctuationTailMatch) {
return punctuationTailMatch[1];
}
return line;
};
const removeExtraArrowheadAfterEdgeLabel = (line: string) => {
// Common typo in generated Mermaid: `-->|label|> Target` (extra `>`).
// Convert it to `-->|label| Target`.
return line.replace(/(\|[^|\n]+\|)\s*>\s*(?=[A-Za-z0-9_("[{'`])/g, "$1 ");
};
const escapeRegExp = (value: string) =>
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const removeLastDeactivateForParticipant = (
sourceText: string,
participant: string,
) => {
const pattern = new RegExp(
`^\\s*deactivate\\s+${escapeRegExp(participant)}(?:\\s+%%.*)?\\s*$`,
);
const lines = sourceText.split(/\r?\n/);
for (let index = lines.length - 1; index >= 0; index--) {
if (pattern.test(lines[index])) {
return lines.filter((_, lineIndex) => lineIndex !== index).join("\n");
}
}
return null;
};
const removeAllDeactivateForParticipant = (
sourceText: string,
participant: string,
) => {
const pattern = new RegExp(
`^\\s*deactivate\\s+${escapeRegExp(participant)}(?:\\s+%%.*)?\\s*$`,
);
const lines = sourceText.split(/\r?\n/);
let removedAny = false;
const remainingLines = lines.filter((line) => {
if (!pattern.test(line)) {
return true;
}
removedAny = true;
return false;
});
return removedAny ? remainingLines.join("\n") : null;
};
const appendMissingEnds = (sourceText: string) => {
const subgraphCount = (sourceText.match(/^\s*subgraph\b/gm) || []).length;
const endCount = (sourceText.match(/^\s*end\s*$/gm) || []).length;
const missingCount = subgraphCount - endCount;
if (missingCount <= 0) {
return null;
}
const endings = Array.from({ length: missingCount }, () => "end").join("\n");
return `${sourceText.trimEnd()}\n${endings}`;
};
const normalizeSmartQuotes = (sourceText: string) =>
sourceText.replace(/[“”]/g, '"').replace(/[‘’]/g, "'");
export const getMermaidAutoFixCandidates = (
sourceText: string,
errorMessage: string,
) => {
if (!isMermaidAutoFixableError(errorMessage) || !sourceText.trim()) {
return [];
}
const candidates: string[] = [];
const seen = new Set<string>();
const addCandidate = (candidate: string | null) => {
if (!candidate || candidate === sourceText || seen.has(candidate)) {
return;
}
seen.add(candidate);
candidates.push(candidate);
};
const inactiveParticipant = getMermaidInactiveParticipant(errorMessage);
if (inactiveParticipant) {
addCandidate(
removeLastDeactivateForParticipant(sourceText, inactiveParticipant),
);
// Fallback for repeated invalid inactivations in one diagram.
addCandidate(
removeAllDeactivateForParticipant(sourceText, inactiveParticipant),
);
}
if (isMermaidParseSyntaxError(errorMessage)) {
const lines = sourceText.split(/\r?\n/);
const errorLineIndex = getErrorLineIndex(errorMessage, sourceText);
const lineIndexesToTry =
errorLineIndex == null
? []
: [errorLineIndex, errorLineIndex - 1, errorLineIndex + 1];
for (const lineIndex of lineIndexesToTry) {
addCandidate(
replaceLineAt(lines, lineIndex, (line) =>
stripTrailingTokenAfterShape(line),
),
);
addCandidate(
replaceLineAt(lines, lineIndex, (line) =>
removeExtraArrowheadAfterEdgeLabel(line),
),
);
}
// Also try full-text replacement so repeated occurrences on other lines
// are fixed together in a single candidate.
addCandidate(removeExtraArrowheadAfterEdgeLabel(sourceText));
addCandidate(appendMissingEnds(sourceText));
const normalizedQuotes = normalizeSmartQuotes(sourceText);
addCandidate(normalizedQuotes === sourceText ? null : normalizedQuotes);
}
return candidates;
};
@@ -0,0 +1,155 @@
import { describe, expect, it } from "vitest";
import {
formatMermaidParseErrorMessage,
getMermaidErrorLineNumber,
getMermaidInactiveParticipant,
getMermaidSyntaxErrorGuidance,
isMermaidAutoFixableError,
isMermaidParseSyntaxError,
isMermaidCaretLine,
} from "./mermaidError";
describe("formatMermaidParseErrorMessage", () => {
it("strips the noisy Expecting clause from Mermaid parse errors", () => {
const message = `Parse error on line 6:
... Control Protocol)"]x L3_UDP
----------------------^
Expecting 'SEMI', 'NEWLINE', 'SPACE', got 'NODE_STRING'`;
expect(formatMermaidParseErrorMessage(message)).toBe(`Parse error on line 6:
... Control Protocol)"]x L3_UDP
----------------------^`);
});
it("keeps Mermaid parse errors unchanged when no Expecting clause exists", () => {
const message = `Parse error on line 3:
... some snippet
----^`;
expect(formatMermaidParseErrorMessage(message)).toBe(message);
});
it("does not modify non-Mermaid parse messages", () => {
const message =
"Unexpected token while parsing JSON. Expecting value at position 10.";
expect(formatMermaidParseErrorMessage(message)).toBe(message);
});
});
describe("isMermaidCaretLine", () => {
it("returns true for Mermaid caret lines", () => {
expect(isMermaidCaretLine("-----------------------^")).toBe(true);
});
it("returns false for regular lines", () => {
expect(isMermaidCaretLine(`... Control Protocol)"]x`)).toBe(false);
});
});
describe("isMermaidParseSyntaxError", () => {
it("returns true for Mermaid parser syntax errors", () => {
expect(isMermaidParseSyntaxError("Parse error on line 6: ...")).toBe(true);
});
it("returns true for Mermaid lexical syntax errors", () => {
expect(
isMermaidParseSyntaxError("Lexical error on line 2. Unrecognized text."),
).toBe(true);
});
it("returns false for non-parse errors", () => {
expect(isMermaidParseSyntaxError("Network error")).toBe(false);
});
});
describe("isMermaidAutoFixableError", () => {
it("returns true for Mermaid parser syntax errors", () => {
expect(isMermaidAutoFixableError("Parse error on line 6: ...")).toBe(true);
});
it("returns true for inactive participant runtime errors", () => {
expect(
isMermaidAutoFixableError(
"Trying to inactivate an inactive participant (QA)",
),
).toBe(true);
});
it("returns false for non-fixable errors", () => {
expect(isMermaidAutoFixableError("Network error")).toBe(false);
});
});
describe("getMermaidInactiveParticipant", () => {
it("extracts the participant id from inactive participant errors", () => {
expect(
getMermaidInactiveParticipant(
"Trying to inactivate an inactive participant (QA)",
),
).toBe("QA");
});
it("returns null for unrelated errors", () => {
expect(
getMermaidInactiveParticipant("Parse error on line 3: ..."),
).toBeNull();
});
});
describe("getMermaidErrorLineNumber", () => {
it("extracts line number from parse error format", () => {
expect(getMermaidErrorLineNumber("Parse error on line 6: ...")).toBe(6);
});
it("extracts line number from lexical error format", () => {
expect(
getMermaidErrorLineNumber("Lexical error on line 2. Unrecognized text."),
).toBe(2);
});
it("returns null for messages without Mermaid line details", () => {
expect(getMermaidErrorLineNumber("Network error")).toBeNull();
});
it("infers line from inactive participant errors when source text is provided", () => {
const sourceText = `sequenceDiagram
participant QA
deactivate QA
QA->>QA: Verifies Fix
deactivate QA`;
expect(
getMermaidErrorLineNumber(
"Trying to inactivate an inactive participant (QA)",
sourceText,
),
).toBe(5);
});
});
describe("getMermaidSyntaxErrorGuidance", () => {
it("returns summary and likely causes for Mermaid parse errors", () => {
const message = `Parse error on line 6:
... Control Protocol)"]x
----------------------^`;
const source = `graph TD
subgraph Layers["X"]
L3_TCP["TCP (Transmission Control Protocol)"]x`;
expect(getMermaidSyntaxErrorGuidance(message, source)).toEqual({
summary: "Syntax error near line 6.",
likelyCauses: expect.arrayContaining([
"A block is missing an `end` statement.",
]),
});
});
it("returns null for non-parse errors", () => {
expect(
getMermaidSyntaxErrorGuidance("Network error", "graph TD"),
).toBeNull();
});
});
@@ -0,0 +1,133 @@
const MERMAID_SYNTAX_ERROR_LINE = /(?:Parse|Lexical) error on line (\d+)[.:]/i;
const MERMAID_INACTIVE_PARTICIPANT_ERROR =
/Trying to inactivate an inactive participant \((.+)\)/i;
const MERMAID_CARET_LINE = /^\s*-+\^\s*$/;
export const isMermaidParseSyntaxError = (message: string) =>
MERMAID_SYNTAX_ERROR_LINE.test(message);
export const isMermaidAutoFixableError = (message: string) =>
isMermaidParseSyntaxError(message) ||
MERMAID_INACTIVE_PARTICIPANT_ERROR.test(message);
export const isMermaidCaretLine = (line: string) =>
MERMAID_CARET_LINE.test(line);
export const getMermaidInactiveParticipant = (
message: string,
): string | null => {
const match = message.match(MERMAID_INACTIVE_PARTICIPANT_ERROR);
if (!match?.[1]) {
return null;
}
return match[1].trim();
};
const escapeRegExp = (value: string) =>
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const getInactiveParticipantLineNumber = (
message: string,
sourceText: string,
): number | null => {
const participant = getMermaidInactiveParticipant(message);
if (!participant) {
return null;
}
const deactivatePattern = new RegExp(
`^\\s*deactivate\\s+${escapeRegExp(participant)}(?:\\s+%%.*)?\\s*$`,
);
const lines = sourceText.split(/\r?\n/);
for (let index = lines.length - 1; index >= 0; index--) {
if (deactivatePattern.test(lines[index])) {
return index + 1;
}
}
return null;
};
export const getMermaidErrorLineNumber = (
message: string,
sourceText?: string,
): number | null => {
const match = message.match(MERMAID_SYNTAX_ERROR_LINE);
if (!match) {
if (!sourceText) {
return null;
}
return getInactiveParticipantLineNumber(message, sourceText);
}
return Number.parseInt(match[1], 10);
};
const countMatches = (text: string, re: RegExp) =>
(text.match(re) || []).length;
export const getMermaidSyntaxErrorGuidance = (
message: string,
sourceText?: string,
): { summary: string; likelyCauses: string[] } | null => {
if (!isMermaidParseSyntaxError(message)) {
return null;
}
const errorLine = getMermaidErrorLineNumber(message, sourceText);
const summary = errorLine
? `Syntax error near line ${errorLine}.`
: "Syntax error in Mermaid diagram.";
const likelyCauses: string[] = [];
if (sourceText) {
const openBrackets = countMatches(sourceText, /\[/g);
const closeBrackets = countMatches(sourceText, /\]/g);
if (openBrackets !== closeBrackets) {
likelyCauses.push("Unbalanced square brackets in a node label.");
}
const openParens = countMatches(sourceText, /\(/g);
const closeParens = countMatches(sourceText, /\)/g);
if (openParens !== closeParens) {
likelyCauses.push("Unbalanced parentheses in a node shape.");
}
const openBraces = countMatches(sourceText, /\{/g);
const closeBraces = countMatches(sourceText, /\}/g);
if (openBraces !== closeBraces) {
likelyCauses.push("Unbalanced braces in a decision node.");
}
const subgraphCount = countMatches(sourceText, /^\s*subgraph\b/gm);
const endCount = countMatches(sourceText, /^\s*end\s*$/gm);
if (subgraphCount > endCount) {
likelyCauses.push("A block is missing an `end` statement.");
}
}
if (/got 'NODE_STRING'/.test(message) || /got 'PS'/.test(message)) {
likelyCauses.push(
"An extra character/token may appear after a node or label definition.",
);
}
if (likelyCauses.length === 0) {
likelyCauses.push(
"A node or edge line is malformed (missing/extra delimiters).",
);
likelyCauses.push("A block (`subgraph`, `class`, etc.) may be incomplete.");
}
return {
summary,
likelyCauses: [...new Set(likelyCauses)],
};
};
export const formatMermaidParseErrorMessage = (message: string) => {
if (!isMermaidParseSyntaxError(message)) {
return message;
}
return message.replace(/\n\s*Expecting[\s\S]*$/, "").trimEnd();
};
+31 -15
View File
@@ -1,35 +1,51 @@
@use "../css/variables.module" as *;
.excalidraw {
.Toast {
$closeButtonSize: 1.2rem;
$closeButtonPadding: 0.4rem;
animation: fade-in 0.5s;
background-color: var(--button-gray-1);
border-radius: 4px;
bottom: 10px;
animation: Toast-fade-in 0.5s;
min-width: 220px;
max-width: min(360px, calc(100vw - 32px));
border-radius: var(--border-radius-lg);
border: 1px solid var(--default-border-color);
background-color: var(--island-bg-color);
color: var(--text-primary-color);
padding: 0.5rem 0.75rem;
box-shadow: 0 0 0 1px var(--color-surface-lowest);
box-sizing: border-box;
cursor: default;
left: 50%;
margin-left: -150px;
padding: 4px 0;
position: absolute;
text-align: center;
width: 300px;
z-index: 999999;
pointer-events: none;
.Toast__message {
font-family: var(--ui-font);
font-size: 0.75rem;
line-height: 1.25rem;
text-align: center;
padding: 0 $closeButtonSize + ($closeButtonPadding);
color: var(--popup-text-color);
white-space: pre-wrap;
}
.Toast__progress-bar {
margin-top: 0.35rem;
width: 100%;
height: 4px;
border-radius: 999px;
background-color: var(--button-gray-2);
overflow: hidden;
}
.Toast__progress-bar-fill {
height: 100%;
border-radius: inherit;
background-color: var(--color-primary);
}
.close {
position: absolute;
top: 0;
right: 0;
padding: $closeButtonPadding;
pointer-events: auto;
.ToolIcon__icon {
width: $closeButtonSize;
@@ -38,7 +54,7 @@
}
}
@keyframes fade-in {
@keyframes Toast-fade-in {
from {
opacity: 0;
}
+18 -4
View File
@@ -5,11 +5,22 @@ import { ToolButton } from "./ToolButton";
import "./Toast.scss";
import type { CSSProperties } from "react";
import type { CSSProperties, ReactNode } from "react";
const DEFAULT_TOAST_TIMEOUT = 5000;
export const Toast = ({
const ProgressBar = ({ progress }: { progress: number }) => (
<div className="Toast__progress-bar">
<div
className="Toast__progress-bar-fill"
style={{
width: `${Math.min(5, Math.round(progress * 100))}%`,
}}
/>
</div>
);
const ToastComponent = ({
message,
onClose,
closable = false,
@@ -17,7 +28,7 @@ export const Toast = ({
duration = DEFAULT_TOAST_TIMEOUT,
style,
}: {
message: string;
message: ReactNode;
onClose: () => void;
closable?: boolean;
duration?: number;
@@ -47,11 +58,12 @@ export const Toast = ({
return (
<div
className="Toast"
role="status"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={style}
>
<p className="Toast__message">{message}</p>
<div className="Toast__message">{message}</div>
{closable && (
<ToolButton
icon={CloseIcon}
@@ -64,3 +76,5 @@ export const Toast = ({
</div>
);
};
export const Toast = Object.assign(ToastComponent, { ProgressBar });
@@ -6,7 +6,6 @@ import {
sceneCoordsToViewportCoords,
type EditorInterface,
} from "@excalidraw/common";
import { AnimationController } from "@excalidraw/excalidraw/renderer/animation";
import type {
InteractiveCanvasRenderConfig,
@@ -24,6 +23,8 @@ import type {
import { t } from "../../i18n";
import { renderInteractiveScene } from "../../renderer/interactiveScene";
import { AnimationController } from "../../renderer/animation";
import type {
AppClassProperties,
AppState,
@@ -469,7 +469,6 @@ const PreferencesToggleMidpointSnappingItem = () => {
return (
<DropdownMenuItemCheckbox
checked={appState.isMidpointSnappingEnabled}
disabled={appState.bindingPreference === "disabled"}
onSelect={(event) => {
actionManager.executeAction(actionToggleMidpointSnapping);
event.preventDefault();
+20
View File
@@ -500,6 +500,26 @@ body.excalidraw-cursor-resize * {
}
}
.floating-status-stack {
position: absolute;
left: 50%;
bottom: 30px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
pointer-events: none;
.scroll-back-to-content {
position: static;
left: auto;
bottom: auto;
transform: none;
pointer-events: var(--ui-pointerEvents);
}
}
.help-icon {
@include outlineButtonStyles;
@include filledButtonOnCanvas;
+10 -9
View File
@@ -27,7 +27,6 @@ import {
import type { AppState, DataURL, LibraryItem } from "../types";
import type { FileSystemHandle } from "browser-fs-access";
import type { ImportedLibraryData } from "./types";
const parseFileContents = async (blob: Blob | File): Promise<string> => {
@@ -104,7 +103,7 @@ export const getMimeType = (blob: Blob | string): string => {
return "";
};
export const getFileHandleType = (handle: FileSystemHandle | null) => {
export const getFileHandleType = (handle: FileSystemFileHandle | null) => {
if (!handle) {
return null;
}
@@ -118,7 +117,9 @@ export const isImageFileHandleType = (
return type === "png" || type === "svg";
};
export const isImageFileHandle = (handle: FileSystemHandle | null) => {
export const isImageFileHandle = (
handle: FileSystemFileHandle | null,
): handle is FileSystemFileHandle => {
const type = getFileHandleType(handle);
return type === "png" || type === "svg";
};
@@ -139,8 +140,8 @@ export const loadSceneOrLibraryFromBlob = async (
/** @see restore.localAppState */
localAppState: AppState | null,
localElements: readonly ExcalidrawElement[] | null,
/** FileSystemHandle. Defaults to `blob.handle` if defined, otherwise null. */
fileHandle?: FileSystemHandle | null,
/** FileSystemFileHandle. Defaults to `blob.handle` if defined, otherwise null. */
fileHandle?: FileSystemFileHandle | null,
) => {
const contents = await parseFileContents(blob);
let data;
@@ -198,8 +199,8 @@ export const loadFromBlob = async (
/** @see restore.localAppState */
localAppState: AppState | null,
localElements: readonly ExcalidrawElement[] | null,
/** FileSystemHandle. Defaults to `blob.handle` if defined, otherwise null. */
fileHandle?: FileSystemHandle | null,
/** FileSystemFileHandle. Defaults to `blob.handle` if defined, otherwise null. */
fileHandle?: FileSystemFileHandle | null,
) => {
const ret = await loadSceneOrLibraryFromBlob(
blob,
@@ -392,7 +393,7 @@ export const ImageURLToFile = async (
export const getFileHandle = async (
event: DragEvent | React.DragEvent | DataTransferItem,
): Promise<FileSystemHandle | null> => {
): Promise<FileSystemFileHandle | null> => {
if (nativeFileSystemSupported) {
try {
const dataTransferItem =
@@ -400,7 +401,7 @@ export const getFileHandle = async (
? event
: (event as DragEvent).dataTransfer?.items?.[0];
const handle: FileSystemHandle | null =
const handle: FileSystemFileHandle | null =
(await (dataTransferItem as any).getAsFileSystemHandle()) || null;
return handle;
+4 -44
View File
@@ -4,18 +4,12 @@ import {
supported as nativeFileSystemSupported,
} from "browser-fs-access";
import { EVENT, MIME_TYPES, debounce } from "@excalidraw/common";
import { AbortError } from "../errors";
import { MIME_TYPES } from "@excalidraw/common";
import { normalizeFile } from "./blob";
import type { FileSystemHandle } from "browser-fs-access";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
const INPUT_CHANGE_INTERVAL_MS = 5000;
export const fileOpen = async <M extends boolean | undefined = false>(opts: {
extensions?: FILE_EXTENSION[];
description: string;
@@ -42,40 +36,6 @@ export const fileOpen = async <M extends boolean | undefined = false>(opts: {
extensions,
mimeTypes,
multiple: opts.multiple ?? false,
legacySetup: (resolve, reject, input) => {
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
const focusHandler = () => {
checkForFile();
document.addEventListener(EVENT.KEYUP, scheduleRejection);
document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
scheduleRejection();
};
const checkForFile = () => {
// this hack might not work when expecting multiple files
if (input.files?.length) {
const ret = opts.multiple ? [...input.files] : input.files[0];
resolve(ret as RetType);
}
};
requestAnimationFrame(() => {
window.addEventListener(EVENT.FOCUS, focusHandler);
});
const interval = window.setInterval(() => {
checkForFile();
}, INPUT_CHANGE_INTERVAL_MS);
return (rejectPromise) => {
clearInterval(interval);
scheduleRejection.cancel();
window.removeEventListener(EVENT.FOCUS, focusHandler);
document.removeEventListener(EVENT.KEYUP, scheduleRejection);
document.removeEventListener(EVENT.POINTER_UP, scheduleRejection);
if (rejectPromise) {
// so that something is shown in console if we need to debug this
console.warn("Opening the file was canceled (legacy-fs).");
rejectPromise(new AbortError());
}
};
},
});
if (Array.isArray(files)) {
@@ -95,8 +55,8 @@ export const fileSave = (
extension: FILE_EXTENSION;
mimeTypes?: string[];
description: string;
/** existing FileSystemHandle */
fileHandle?: FileSystemHandle | null;
/** existing FileSystemFileHandle */
fileHandle?: FileSystemFileHandle | null;
},
) => {
return _fileSave(
@@ -108,8 +68,8 @@ export const fileSave = (
mimeTypes: opts.mimeTypes,
},
opts.fileHandle,
false,
);
};
export { nativeFileSystemSupported };
export type { FileSystemHandle };
+1 -3
View File
@@ -33,8 +33,6 @@ import { canvasToBlob } from "./blob";
import { fileSave } from "./filesystem";
import { serializeAsJSON } from "./json";
import type { FileSystemHandle } from "./filesystem";
import type { ExportType } from "../scene/types";
import type { AppState, BinaryFiles } from "../types";
@@ -110,7 +108,7 @@ export const exportCanvas = async (
viewBackgroundColor: string;
/** filename, if applicable */
name?: string;
fileHandle?: FileSystemHandle | null;
fileHandle?: FileSystemFileHandle | null;
exportingFrame: ExcalidrawFrameLikeElement | null;
},
) => {
+27 -18
View File
@@ -1,12 +1,13 @@
import {
DEFAULT_FILENAME,
EXPORT_DATA_TYPES,
getExportSource,
MIME_TYPES,
VERSIONS,
} from "@excalidraw/common";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type { ExcalidrawElement, NonDeleted } from "@excalidraw/element/types";
import type { MaybePromise } from "@excalidraw/common/utility-types";
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
@@ -21,6 +22,12 @@ import type {
ImportedLibraryData,
} from "./types";
export type JSONExportData = {
elements: readonly NonDeleted<ExcalidrawElement>[];
appState: AppState;
files: BinaryFiles;
};
/**
* Strips out files which are only referenced by deleted elements
*/
@@ -67,27 +74,29 @@ export const serializeAsJSON = (
return JSON.stringify(data, null, 2);
};
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
files: BinaryFiles,
/** filename */
name: string = appState.name || DEFAULT_FILENAME,
) => {
const serialized = serializeAsJSON(elements, appState, files, "local");
const blob = new Blob([serialized], {
type: MIME_TYPES.excalidraw,
export const saveAsJSON = async ({
data,
filename,
fileHandle,
}: {
data: MaybePromise<JSONExportData>;
filename: string;
fileHandle: AppState["fileHandle"];
}) => {
const blob = Promise.resolve(data).then(({ elements, appState, files }) => {
const serialized = serializeAsJSON(elements, appState, files, "local");
return new Blob([serialized], {
type: MIME_TYPES.excalidraw,
});
});
const fileHandle = await fileSave(blob, {
name,
const savedFileHandle = await fileSave(blob, {
name: filename,
extension: "excalidraw",
description: "Excalidraw file",
fileHandle: isImageFileHandle(appState.fileHandle)
? null
: appState.fileHandle,
fileHandle: isImageFileHandle(fileHandle) ? null : fileHandle,
});
return { fileHandle };
return { fileHandle: savedFileHandle };
};
export const loadFromJSON = async (
+21 -8
View File
@@ -1,26 +1,39 @@
import type { MaybePromise } from "@excalidraw/common/utility-types";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import { getFileHandleType, isImageFileHandleType } from "./blob";
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { exportCanvas, prepareElementsForExport } from ".";
import type { AppState, BinaryFiles } from "../types";
export const resaveAsImageWithScene = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
files: BinaryFiles,
name: string,
data: MaybePromise<{
elements: readonly ExcalidrawElement[];
appState: AppState;
files: BinaryFiles;
}>,
fileHandle: FileSystemFileHandle,
filename: string,
) => {
const { exportBackground, viewBackgroundColor, fileHandle } = appState;
const fileHandleType = getFileHandleType(fileHandle);
if (!fileHandle || !isImageFileHandleType(fileHandleType)) {
if (Math.random() < 1) {
throw new Error("OLALALALA");
}
if (!isImageFileHandleType(fileHandleType)) {
throw new Error(
"fileHandle should exist and should be of type svg or png when resaving",
);
}
let { elements, appState, files } = await data;
const { exportBackground, viewBackgroundColor } = appState;
appState = {
...appState,
exportEmbedScene: true,
@@ -35,7 +48,7 @@ export const resaveAsImageWithScene = async (
await exportCanvas(fileHandleType, exportedElements, appState, files, {
exportBackground,
viewBackgroundColor,
name,
name: filename,
fileHandle,
exportingFrame,
});
+1 -1
View File
@@ -52,7 +52,7 @@ declare module "png-chunks-extract" {
// -----------------------------------------------------------------------------
interface Blob {
handle?: import("browser-fs-acces").FileSystemHandle;
handle?: FileSystemFileHandle;
name?: string;
}
@@ -0,0 +1,110 @@
import { useEffect, useRef, useState } from "react";
import { useExcalidrawAPI } from "../components/App";
import { getDefaultAppState } from "../appState";
import type { AppState } from "../types";
/**
* Subscribes to specific appState changes. The component re-renders
* only when the specified prop(s) change — not on every appState update.
*
* Works both inside and outside the <Excalidraw> tree, as long as
* ExcalidrawAPIContext.Provider is an ancestor (automatically provided
* inside <Excalidraw>, or manually by the host app).
*
* Returns the narrowed value depending on prop form:
* - `keyof AppState` → `AppState[K]`
* - `(keyof AppState)[]` → whole `AppState`
* - selector function → selector's return type `T`
*
* Calls the optional callback with the latest value on every change (not called
* on initial render).
*
* If excalidrawAPI is not ready yet (host apps), hook is rerendered with latest
* value once available.
*/
export function useAppStateValue<K extends keyof AppState>(
prop: K,
callback?: (value: AppState[K], appState: AppState) => void,
_internal?: boolean,
): AppState[K];
export function useAppStateValue(
props: (keyof AppState)[],
callback?: (props: AppState, appState: AppState) => void,
_internal?: boolean,
): AppState;
export function useAppStateValue<T>(
selector: (appState: AppState) => T,
callback?: (value: T, appState: AppState) => void,
_internal?: boolean,
): T;
export function useAppStateValue(
selector:
| keyof AppState
| (keyof AppState)[]
| ((appState: AppState) => unknown),
callback?: (value: any, appState: AppState) => void,
_internal: boolean = true,
): unknown {
const api = useExcalidrawAPI();
const getLatestValue = () => {
let appState = api?.getAppState();
if (!appState) {
if (!_internal) {
return undefined;
}
console.warn(
"useAppStateValue: excalidrawAPI not defined yet for internal component in which case it should always be defined. Are you sure you're rendering inside of <Excalidraw/> component tree?",
);
// fall back in case there's a bug so we don't break the app
// (internal components using this internal useAppStateValue expect
// non-undefined values on init)
appState = Object.assign(
{ width: 0, height: 0, offsetLeft: 0, offsetTop: 0 },
getDefaultAppState(),
);
}
if (typeof selector === "function") {
return selector(appState);
}
if (Array.isArray(selector)) {
return appState;
}
return appState[selector];
};
const [value, setValue] = useState<unknown>(getLatestValue);
const stateRef = useRef({
selector,
callback,
isInitialized: !!api,
latestValue: value,
});
stateRef.current.selector = selector;
stateRef.current.callback = callback;
if (!stateRef.current.isInitialized && api) {
stateRef.current.isInitialized = true;
stateRef.current.latestValue = getLatestValue();
}
useEffect(() => {
if (!api) {
return;
}
return api.onStateChange(
stateRef.current.selector,
(newValue: any, state: AppState) => {
stateRef.current.latestValue = newValue;
stateRef.current.callback?.(newValue, state);
setValue(newValue);
},
);
}, [api]);
return stateRef.current.latestValue;
}
+91 -6
View File
@@ -1,14 +1,18 @@
import React, { useEffect } from "react";
import React, { useCallback, useContext, useEffect, useState } from "react";
import { DEFAULT_UI_OPTIONS, isShallowEqual } from "@excalidraw/common";
import App from "./components/App";
import App, {
ExcalidrawAPIContext,
ExcalidrawAPISetContext,
} from "./components/App";
import { InitializeApp } from "./components/InitializeApp";
import Footer from "./components/footer/FooterCenter";
import LiveCollaborationTrigger from "./components/live-collaboration/LiveCollaborationTrigger";
import MainMenu from "./components/main-menu/MainMenu";
import WelcomeScreen from "./components/welcome-screen/WelcomeScreen";
import { defaultLang } from "./i18n";
import { useAppStateValue as _useAppStateValue } from "./hooks/useAppStateValue";
import { EditorJotaiProvider, editorJotaiStore } from "./editor-jotai";
import polyfill from "./polyfill";
@@ -16,16 +20,44 @@ import "./css/app.scss";
import "./css/styles.scss";
import "./fonts/fonts.css";
import type { AppProps, ExcalidrawProps } from "./types";
import type {
AppProps,
AppState,
ExcalidrawImperativeAPI,
ExcalidrawProps,
} from "./types";
polyfill();
/**
* Stateless provider that allows `useExcalidrawAPI()` (and hooks built
* on it, such as `useAppStateValue()`) to work outside the <Excalidraw>
* component tree.
*/
export const ExcalidrawAPIProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [api, setApi] = useState<ExcalidrawImperativeAPI | null>(null);
return (
<ExcalidrawAPIContext.Provider value={api}>
<ExcalidrawAPISetContext.Provider value={setApi}>
{children}
</ExcalidrawAPISetContext.Provider>
</ExcalidrawAPIContext.Provider>
);
};
const ExcalidrawBase = (props: ExcalidrawProps) => {
const {
onExport,
onChange,
onIncrement,
initialData,
excalidrawAPI,
onExcalidrawAPI,
onMount,
onInitialize,
isCollaborating = false,
onPointerUpdate,
renderTopLeftUI,
@@ -86,6 +118,15 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
UIOptions.canvasActions.toggleTheme = true;
}
const setExcalidrawAPI = useContext(ExcalidrawAPISetContext);
const handleExcalidrawAPI = useCallback(
(api: ExcalidrawImperativeAPI) => {
setExcalidrawAPI?.(api);
onExcalidrawAPI?.(api);
},
[onExcalidrawAPI, setExcalidrawAPI],
);
useEffect(() => {
const importPolyfill = async () => {
//@ts-ignore
@@ -115,10 +156,13 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
<EditorJotaiProvider store={editorJotaiStore}>
<InitializeApp langCode={langCode} theme={theme}>
<App
onExport={onExport}
onChange={onChange}
onIncrement={onIncrement}
initialData={initialData}
excalidrawAPI={excalidrawAPI}
onExcalidrawAPI={handleExcalidrawAPI}
onMount={onMount}
onInitialize={onInitialize}
isCollaborating={isCollaborating}
onPointerUpdate={onPointerUpdate}
renderTopLeftUI={renderTopLeftUI}
@@ -267,6 +311,7 @@ export {
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
getFormFactor,
throttleRAF,
} from "@excalidraw/common";
export {
@@ -284,7 +329,13 @@ export { Button } from "./components/Button";
export { Footer };
export { MainMenu };
export { Ellipsify } from "./components/Ellipsify";
export { useEditorInterface, useStylesPanelMode } from "./components/App";
export {
useEditorInterface,
useStylesPanelMode,
useExcalidrawAPI,
ExcalidrawAPIContext,
} from "./components/App";
export { WelcomeScreen };
export { LiveCollaborationTrigger };
export { Stats } from "./components/Stats";
@@ -325,3 +376,37 @@ export {
tryParseSpreadsheet,
isSpreadsheetValidForChartType,
} from "./charts";
// -----------------------------------------------------------------------------
// useAppStateValue() wrapper for host apps for the return type to reflect the
// the potentially `undefined` value for initial render before the excalidrawAPI
// is ready.
//
/**
* hook that subscribes to specific appState prop(s)
*
* @param prop - appState prop(s) to subscribe to, or a selector function.
* NOTE `prop/selector` is memoized and will not change after initial render
*/
export function useAppStateValue<K extends keyof AppState>(
prop: K,
callback?: (value: AppState[K], appState: AppState) => void,
): AppState[K] | undefined;
export function useAppStateValue<T extends keyof AppState>(
props: T[],
callback?: (values: AppState, appState: AppState) => void,
): AppState | undefined;
export function useAppStateValue<T>(
selector: (appState: AppState) => T,
callback?: (value: T, appState: AppState) => void,
): T | undefined;
export function useAppStateValue(
selector:
| keyof AppState
| (keyof AppState)[]
| ((appState: AppState) => unknown),
callback?: (value: any, appState: AppState) => void,
) {
return _useAppStateValue(selector as any, callback, false);
}
// -----------------------------------------------------------------------------
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "لصق",
"pasteAsPlaintext": "اللصق كنص عادي",
"pasteCharts": "لصق الرسوم البيانية",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "تحديد الكل",
"multiSelect": "إضافة عنصر للتحديد",
"moveCanvas": "نقل لوح الرسم",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Crow's foot (many)",
"arrowhead_crowfoot_one": "Crow's foot (one)",
"arrowhead_crowfoot_one_or_many": "Crow's foot (one or many)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "خيارات إضافية",
"cardinality": "",
"arrowtypes": "نوع السهم",
"arrowtype_sharp": "سهم حاد",
"arrowtype_round": "سهم منحني",
@@ -182,11 +171,7 @@
"linkToElement": "رابط إلى الكائن",
"wrapSelectionInFrame": "تغليف التحديد في إطار",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "رابط إلى الكائن",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "خطأ"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "حفظ الملف على الجهاز",
"disk_details": "تصدير بيانات المشهد إلى ملف يمكنك الاستيراد منه لاحقًا.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid إلى Excalidraw",
"button": "إدراج",
"description": "",
"description": "حاليًا، يتم دعم <flowchartLink>مخططات التدفق</flowchartLink>، <sequenceLink>التسلسلات</sequenceLink>، و<classLink>الفئات</classLink> فقط. سيتم عرض الأنواع الأخرى كصورة في Excalidraw.",
"syntax": "صيغة Mermaid",
"preview": "معاينة",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Yapışdır",
"pasteAsPlaintext": "Düz mətn kimi yapışdırın",
"pasteCharts": "Diaqramları yapışdırın",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Hamısını seç",
"multiSelect": "Seçimə element əlavə edin",
"moveCanvas": "Kanvası köçürün",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": ""
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Постави",
"pasteAsPlaintext": "Постави като обикновен текст",
"pasteCharts": "Постави графики",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Маркирай всичко",
"multiSelect": "Добави елемент към селекция",
"moveCanvas": "Премести платно",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "Вид стрелка",
"arrowtype_sharp": "Остра стрелка",
"arrowtype_round": "Извита стрелка",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Грешка"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Запази към диск",
"disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "Mermaid Синтаксис",
"preview": "Преглед",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "পেস্ট করুন",
"pasteAsPlaintext": "প্লেইনটেক্সট হিসাবে পেস্ট করুন",
"pasteCharts": "চার্ট পেস্ট করুন",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "সবটা সিলেক্ট করুন",
"multiSelect": "একাধিক সিলেক্ট করুন",
"moveCanvas": "ক্যানভাস সরান",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "ত্রুটি"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "পেস্ট করুন",
"pasteAsPlaintext": "প্লেইনটেক্সট হিসাবে পেস্ট করুন",
"pasteCharts": "চার্ট পেস্ট করুন",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "সবটা সিলেক্ট করুন",
"multiSelect": "একাধিক সিলেক্ট করুন",
"moveCanvas": "ক্যানভাস সরান",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "ত্রুটি"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Enganxa",
"pasteAsPlaintext": "Enganxar com a text pla",
"pasteCharts": "Enganxa els diagrames",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Selecciona-ho tot",
"multiSelect": "Afegeix un element a la selecció",
"moveCanvas": "Mou el llenç",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Potes de gall (moltes)",
"arrowhead_crowfoot_one": "Potes de gall (una)",
"arrowhead_crowfoot_one_or_many": "Potes de gall (una o moltes)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Més opcions",
"cardinality": "",
"arrowtypes": "Tipus de fletxa",
"arrowtype_sharp": "Fletxa Esmolada",
"arrowtype_round": "Fletxa corba",
@@ -182,11 +171,7 @@
"linkToElement": "Enllaç a l'objecte",
"wrapSelectionInFrame": "Embolica la selecció en un marc",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "Enllaç a l'objecte",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Error"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Desa al disc",
"disk_details": "Exporta les dades de l'escena a un fitxer que després podreu importar.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid a Excalidraw",
"button": "Inseriu",
"description": "",
"description": "Actualment només s'admeten els diagrames <flowchartLink>Flowchart</flowchartLink>, <sequenceLink> Sequence, </sequenceLink> i <classLink> Class </classLink>. Els altres tipus es representaran com a imatge a Excalidraw.",
"syntax": "Sintaxi de Mermaid",
"preview": "Previsualització",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Vložit",
"pasteAsPlaintext": "Vložit jako prostý text",
"pasteCharts": "Vložit grafy",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Vybrat vše",
"multiSelect": "Přidat prvek do výběru",
"moveCanvas": "Posunout plátno",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "Typ šipky",
"arrowtype_sharp": "Ostrá šipka",
"arrowtype_round": "Zakřivená šipka",
@@ -182,11 +171,7 @@
"linkToElement": "Odkaz na objekt",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "Odkaz na objekt",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Chyba"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Uložit na disk",
"disk_details": "Exportovat data scény do souboru, ze kterého můžete importovat později.",
@@ -635,8 +616,7 @@
"syntax": "Mermaid syntaxe",
"preview": "Náhled",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Indsæt",
"pasteAsPlaintext": "Indsæt som klartekst",
"pasteCharts": "Indsæt diagrammer",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Marker alle",
"multiSelect": "Tilføj element til markering",
"moveCanvas": "Flyt lærred",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Kragefod (mange)",
"arrowhead_crowfoot_one": "Kragefod (én)",
"arrowhead_crowfoot_one_or_many": "Kragefod (én eller mange)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Flere muligheder",
"cardinality": "",
"arrowtypes": "Pile type",
"arrowtype_sharp": "Skarp pil",
"arrowtype_round": "Buet pil",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Fejl"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Gem til disk",
"disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Einfügen",
"pasteAsPlaintext": "Als unformatierten Text einfügen",
"pasteCharts": "Diagramme einfügen",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Alle auswählen",
"multiSelect": "Element zur Auswahl hinzufügen",
"moveCanvas": "Leinwand verschieben",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Krähenfuß (viele)",
"arrowhead_crowfoot_one": "Krähenfuß (einer)",
"arrowhead_crowfoot_one_or_many": "Krähenfuß (einer oder viele)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Weitere Optionen",
"cardinality": "",
"arrowtypes": "Pfeiltyp",
"arrowtype_sharp": "Scharfer Pfeil",
"arrowtype_round": "Gebogener Pfeil",
@@ -182,11 +171,7 @@
"linkToElement": "Link zum Objekt",
"wrapSelectionInFrame": "Auswahl in Rahmen einbetten",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "Link zum Objekt",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Fehler"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Auf Festplatte speichern",
"disk_details": "Exportiere die Zeichnungsdaten in eine Datei, die Du später importieren kannst.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid zu Excalidraw",
"button": "Einfügen",
"description": "",
"description": "Derzeit werden nur <flowchartLink>Flussdiagramme</flowchartLink>, <sequenceLink>Sequenzdiagramme</sequenceLink> und <classLink>Klassendiagramme</classLink> unterstützt. Die anderen Typen werden als Bild in Excalidraw dargestellt.",
"syntax": "Mermaid-Syntax",
"preview": "Vorschau",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Einfügen",
"pasteAsPlaintext": "Als unformatierten Text einfügen",
"pasteCharts": "Diagramme einfügen",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Alle auswählen",
"multiSelect": "Element zur Auswahl hinzufügen",
"moveCanvas": "Leinwand verschieben",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Krähenfuß (viele)",
"arrowhead_crowfoot_one": "Krähenfuß (einer)",
"arrowhead_crowfoot_one_or_many": "Krähenfuß (einer oder viele)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Weitere Optionen",
"cardinality": "",
"arrowtypes": "Pfeiltyp",
"arrowtype_sharp": "Scharfer Pfeil",
"arrowtype_round": "Gebogener Pfeil",
@@ -182,11 +171,7 @@
"linkToElement": "Link zum Objekt",
"wrapSelectionInFrame": "Auswahl in Rahmen einbetten",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "Link zum Objekt",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Fehler"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Auf Festplatte speichern",
"disk_details": "Exportiere die Zeichnungsdaten in eine Datei, die Du später importieren kannst.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid zu Excalidraw",
"button": "Einfügen",
"description": "",
"description": "Derzeit werden nur <flowchartLink>Flussdiagramme</flowchartLink>, <sequenceLink>Sequenzdiagramme</sequenceLink> und <classLink>Klassendiagramme</classLink> unterstützt. Die anderen Typen werden als Bild in Excalidraw dargestellt.",
"syntax": "Mermaid-Syntax",
"preview": "Vorschau",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Επικόλληση",
"pasteAsPlaintext": "Επικόλληση ως απλό κείμενο",
"pasteCharts": "Επικόλληση γραφημάτων",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Επιλογή όλων",
"multiSelect": "Προσθέστε το στοιχείο στην επιλογή",
"moveCanvas": "Μετακίνηση καμβά",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "Τύπος βέλους",
"arrowtype_sharp": "Αιχμηρό βέλος",
"arrowtype_round": "Κυρτό βέλος",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Σφάλμα"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Αποθήκευση στο δίσκο",
"disk_details": "Εξαγωγή δεδομένων σκηνής σε ένα αρχείο από το οποίο μπορείτε να εισάγετε αργότερα.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid σε Excalidraw",
"button": "Εισαγωγή",
"description": "",
"description": "Επί του παρόντος υποστηρίζονται μόνο Διαγράμματα <flowchartLink>Ροής</flowchartLink>,<sequenceLink> Ακολουθίας, </sequenceLink> και <classLink>Κλάσεων</classLink>. Οι άλλοι τύποι θα αποδοθούν ως εικόνα στο Excalidraw.",
"syntax": "Σύνταξη Mermaid",
"preview": "Προεπισκόπηση",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+6 -1
View File
@@ -403,6 +403,10 @@
"errorDialog": {
"title": "Error"
},
"progressDialog": {
"title": "Saving",
"defaultMessage": "Preparing to save..."
},
"exportDialog": {
"disk_title": "Save to disk",
"disk_details": "Export the scene data to a file from which you can import later.",
@@ -624,7 +628,8 @@
"syntax": "Mermaid Syntax",
"preview": "Preview",
"label": "Mermaid",
"inputPlaceholder": "Write Mermaid diagram defintion here..."
"inputPlaceholder": "Write Mermaid diagram defintion here...",
"autoFixAvailable": "Auto-fix is available"
},
"ttd": {
"error": "Error!"
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Pegar",
"pasteAsPlaintext": "Pegar como texto sin formato",
"pasteCharts": "Pegar gráficos",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Seleccionar todo",
"multiSelect": "Añadir elemento a la selección",
"moveCanvas": "Mover el lienzo",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Pie de la corona (varios)",
"arrowhead_crowfoot_one": "Pie de la corona (uno)",
"arrowhead_crowfoot_one_or_many": "Pie de la corona (uno o varios)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Más opciones",
"cardinality": "",
"arrowtypes": "Tipo de flecha",
"arrowtype_sharp": "Flecha Afilada",
"arrowtype_round": "Flecha Curva",
@@ -182,11 +171,7 @@
"linkToElement": "Enlace al objeto",
"wrapSelectionInFrame": "Ajustar la selección en el marco",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "Enlace al objeto",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Error"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Guardar en disco",
"disk_details": "Exportar los datos de la escena a un archivo desde el cual pueda importar más tarde.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid a Excalidraw",
"button": "Insertar",
"description": "",
"description": "Actualmente sólo estos tipos de <flowchartLink>diagrama de flujo</flowchartLink>,<sequenceLink> Secuencia, </sequenceLink> y <classLink>Clase </classLink> son soportados. Los otros tipos de diagramas se renderizarán como imagen en Excalidraw.",
"syntax": "Sintaxis Mermaid",
"preview": "Vista previa",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Itsatsi",
"pasteAsPlaintext": "Itsatsi testu arrunt gisa",
"pasteCharts": "Itsatsi grafikoak",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Hautatu dena",
"multiSelect": "Gehitu elementua hautapenera",
"moveCanvas": "Mugitu oihala",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Errorea"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Gorde diskoan",
"disk_details": "Esportatu eszenaren datuak geroago inportatu ahal izango duzun fitxategi batan.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid-etik Excalidraw-ra",
"button": "Txertatu",
"description": "",
"description": "Momentu honetan <flowchartLink>Flowchart</flowchartLink>, <sequenceLink> Sequence, </sequenceLink> eta <classLink>Class </classLink>Diagramak onartzen dira. Beste motak irudi gisa errendatuko dira Excalidrawn.",
"syntax": "Mermaid sintaxia",
"preview": "Aurrebista",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+11 -31
View File
@@ -3,10 +3,6 @@
"paste": "جایگذاری",
"pasteAsPlaintext": "جایگذاری به عنوان متن ساده",
"pasteCharts": "جایگذاری نمودارها",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "انتخاب همه",
"multiSelect": "یک ایتم به انتخاب شده ها اضافه کنید",
"moveCanvas": "جابجایی بوم",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "پای کلاغی (بسیار)",
"arrowhead_crowfoot_one": "پای کلاغی (یک)",
"arrowhead_crowfoot_one_or_many": "پای کلاغی (یک یا بسیار)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "امکانات بیشتر",
"cardinality": "",
"arrowtypes": "نوع پیکان",
"arrowtype_sharp": "پیکان تیز",
"arrowtype_round": "پیکان منحنی",
@@ -182,11 +171,7 @@
"linkToElement": "لینک به آیتم",
"wrapSelectionInFrame": "انتخاب را در قاب قرار دهید",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "لینک به آیتم",
@@ -211,7 +196,7 @@
"multipleResults": "نتایج",
"placeholder": "جستجوی متن در بوم...",
"frames": "",
"texts": "متن"
"texts": ""
},
"buttons": {
"clearReset": "پاکسازی بوم نقاشی",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "خطا"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "ذخیره در دیسک",
"disk_details": "داده های صحنه را به فایلی که بعداً می توانید از آن وارد کنید صادر کنید.",
@@ -590,7 +571,7 @@
}
},
"colorPicker": {
"color": "رنگ",
"color": "",
"mostUsedCustomColors": "رنگ های به‌تازگی به‌کار گرفته شده",
"colors": "رنگ‌ها",
"shades": "جلوه‌ها",
@@ -631,15 +612,14 @@
"mermaid": {
"title": "مرمید به excalidraw",
"button": "درج",
"description": "",
"description": "فعلا فقط <flowchartLink> فلوچارت </flowchartLink> ، <sequenceLink> توالی </sequenceLink> و <classLink> کلاس </classLink> نمودارها پشتیبانی می شوند. انواع دیگر به صورت تصویر در Excalidraw ارائه خواهند شد.",
"syntax": "مرمید syntax",
"preview": "پیشنمایش",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": "خطا!"
"error": ""
},
"chat": {
"inputPlaceholder": "",
@@ -647,8 +627,8 @@
"generating": "",
"rateLimitRemaining": "",
"role": {
"user": "شما",
"assistant": "دستیار هوش مصنوعی",
"user": "",
"assistant": "",
"system": ""
},
"aiBeta": "",
@@ -727,9 +707,9 @@
"alt": "",
"escape": "",
"enter": "",
"shift": "Shift",
"spacebar": "فاصله",
"delete": "حذف",
"shift": "",
"spacebar": "",
"delete": "",
"mmb": ""
}
}
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Liitä",
"pasteAsPlaintext": "Liitä pelkkänä tekstinä",
"pasteCharts": "Liitä kaaviot",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Valitse kaikki",
"multiSelect": "Lisää kohde valintaan",
"moveCanvas": "Siirrä piirtoaluetta",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Virhe"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Tallenna levylle",
"disk_details": "Vie työn tiedot tiedostoon, josta sen voi tuoda myöhemmin.",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "Esikatsele",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+40 -60
View File
@@ -3,10 +3,6 @@
"paste": "Coller",
"pasteAsPlaintext": "Coller comme texte brut",
"pasteCharts": "Coller les graphiques",
"chartType_bar": "Graphique à barres",
"chartType_line": "Courbes",
"chartType_radar": "Diagramme en radar",
"chartType_plaintext": "Texte brut",
"selectAll": "Tout sélectionner",
"multiSelect": "Ajouter l'élément à la sélection",
"moveCanvas": "Déplacer le canevas",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "Pied de Corde (un)",
"arrowhead_crowfoot_one_or_many": "Pied du corbeau (un ou plusieurs)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Plus d'options",
"cardinality": "",
"arrowtypes": "Type de flèche",
"arrowtype_sharp": "Flèche pointue",
"arrowtype_round": "Flèche incurvée",
@@ -182,11 +171,7 @@
"linkToElement": "Lien vers un objet",
"wrapSelectionInFrame": "Mettre la sélection dans un cadre",
"tab": "Onglet",
"shapeSwitch": "Changer de forme",
"preferences": "Préférences",
"preferences_toolLock": "Verrouillage de l'outil",
"arrowBinding": "Liaison de flèches",
"midpointSnapping": "S'accrocher aux points intermédiaires"
"shapeSwitch": "Changer de forme"
},
"elementLink": {
"title": "Lien vers un objet",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Erreur"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Enregistrer sur le disque",
"disk_details": "Exporter les données de la scène comme un fichier que vous pourrez importer ultérieurement.",
@@ -576,9 +557,9 @@
},
"welcomeScreen": {
"app": {
"center_heading": "Vos dessins sont enregistrés dans le stockage de votre navigateur.",
"center_heading_line2": "Le stockage du navigateur peut être effacé de manière inattendue.",
"center_heading_line3": "Enregistrez régulièrement votre travail dans un fichier pour éviter de le perdre.",
"center_heading": "",
"center_heading_line2": "",
"center_heading_line3": "",
"center_heading_plus": "Vouliez-vous plutôt aller à Excalidraw+ à la place ?",
"menuHint": "Exportation, préférences, langues, ..."
},
@@ -631,58 +612,57 @@
"mermaid": {
"title": "De Mermaid à Excalidraw",
"button": "Insérer",
"description": "",
"description": "Actuellement, seuls les diagrammes <flowchartLink>Flowchart</flowchartLink>,<sequenceLink> Sequence, </sequenceLink> et <classLink>de classe </classLink>sont pris en charge. Les autres types seront rendus en tant qu'image dans Excalidraw.",
"syntax": "Syntaxe Mermaid",
"preview": "Prévisualisation",
"label": "Mermaid",
"inputPlaceholder": "Écrivez la définition du diagramme de Mermaid ici...",
"autoFixAvailable": "Correction automatique disponible"
"label": "",
"inputPlaceholder": ""
},
"ttd": {
"error": "Erreur!"
"error": ""
},
"chat": {
"inputPlaceholder": "Commencez à taper votre idée de diagramme ici... ({{shortcut}} pour une nouvelle ligne)",
"inputPlaceholderWithMessages": "Continuer à affiner votre diagramme...",
"generating": "Génération...",
"rateLimitRemaining": "{{count}} demandes restantes aujourd'hui",
"inputPlaceholder": "",
"inputPlaceholderWithMessages": "",
"generating": "",
"rateLimitRemaining": "",
"role": {
"user": "Vous",
"assistant": "Assistant IA",
"system": "Système"
"user": "",
"assistant": "",
"system": ""
},
"aiBeta": "Bêta IA",
"label": "Chat",
"menu": "Menu",
"newChat": "Nouveau chat",
"deleteChat": "Supprimer le chat",
"deleteMessage": "Supprimer le message",
"viewAsMermaid": "Voir en tant que Mermaid",
"aiBeta": "",
"label": "",
"menu": "",
"newChat": "",
"deleteChat": "",
"deleteMessage": "",
"viewAsMermaid": "",
"placeholder": {
"title": "Créons votre diagramme",
"description": "Décrivez le diagramme que vous voulez créer, et nous le générerons pour vous.",
"title": "",
"description": "",
"hint": ""
},
"preview": "Prévisualisation",
"insert": "Insérer",
"retry": "Recommencer",
"preview": "",
"insert": "",
"retry": "",
"errors": {
"promptTooShort": "La requête est trop courte (min {{min}} caractères)",
"promptTooLong": "La requête est trop longue (max {{max}} caractères)",
"generationFailed": "Échec de la génération",
"invalidDiagram": "A généré un diagramme non valide :(. Vous pouvez modifier manuellement, réessayer avec la correction automatique, ou essayer une invite différente.",
"fixInMermaid": "Modifier Mermaid manuellement →",
"aiRepair": "Régénérer (correction automatique) →",
"requestAborted": "Demande abandonnée",
"requestFailed": "La requête a échoué",
"mermaidParseError": "Erreur de syntaxe Mermaid"
"promptTooShort": "",
"promptTooLong": "",
"generationFailed": "",
"invalidDiagram": "",
"fixInMermaid": "",
"aiRepair": "",
"requestAborted": "",
"requestFailed": "",
"mermaidParseError": ""
},
"rateLimit": {
"messageLimit": "Vous avez atteint votre limite d'IA sur le plan gratuit. Essayez Excalidraw+ pour plus ou revenez demain.",
"generalRateLimit": "Tenez vos chevaux, vous êtes trop rapide pour nous! Veuillez attendre un instant avant de réessayer.",
"messageLimitInputPlaceholder": "Vous avez atteint votre limite de messages"
"messageLimit": "",
"generalRateLimit": "",
"messageLimitInputPlaceholder": ""
},
"upsellBtnLabel": "Devenez Premium"
"upsellBtnLabel": ""
},
"quickSearch": {
"placeholder": "Recherche rapide"
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Pegar",
"pasteAsPlaintext": "Pegar coma texto sen formato",
"pasteCharts": "Pegar gráficos",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Seleccionar todo",
"multiSelect": "Engadir elemento á selección",
"moveCanvas": "Mover o lenzo",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Erro"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Gardar no disco",
"disk_details": "Exporte os datos da escena a un ficheiro que poderás importar máis tarde.",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "Vista previa",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "הדבקה",
"pasteAsPlaintext": "הדבקה ללא עיצוב",
"pasteCharts": "הדבקת תרשימים",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "בחירה בהכול",
"multiSelect": "הוספת רכיב לבחירה",
"moveCanvas": "הזזת הקנבס",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "רגל עורב (הרבה)",
"arrowhead_crowfoot_one": "רגל עורב (אחת)",
"arrowhead_crowfoot_one_or_many": "רגל עורב (אחת או הרבה)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "אפשרויות נוספות",
"cardinality": "",
"arrowtypes": "סוג החץ",
"arrowtype_sharp": "חץ מחודד",
"arrowtype_round": "חץ מעוקל",
@@ -182,11 +171,7 @@
"linkToElement": "קישור לפריט",
"wrapSelectionInFrame": "לעטוף את הבחירה במסגרת",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "קישור לפריט",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "שגיאה"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "שמור לכונן",
"disk_details": "ייצא מידע של הקנבאס לקובץ שתוכל לייבא אחר כך.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid ל־Excalidraw",
"button": "הוספה",
"description": "",
"description": "לעת עתה נתמכים רק <flowchartLink>תרשימי זרימה</flowchartLink>, <sequenceLink>תהליכים</sequenceLink>, <classLink>ודיאגרמת מחלקה</classLink>. שאר הסוגים ייוצרו כתמונות ב-Excalidraw.",
"syntax": "תחביר Mermaid",
"preview": "תצוגה מקדימה",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "पेस्ट",
"pasteAsPlaintext": "सादे पाठ के रूप में चिपकाएं",
"pasteCharts": "चार्ट चिपकाएँ",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "सेलेक्ट ऑल",
"multiSelect": "अवयव को चयन में सम्मिलित करें",
"moveCanvas": "चित्रपटल को स्थानांतरित करें",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "चिड़िया पैर (अनेक)",
"arrowhead_crowfoot_one": "चिड़िया पैर (एक)",
"arrowhead_crowfoot_one_or_many": "चिड़िया पैर (एक या अनेक)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "और विकल्प",
"cardinality": "",
"arrowtypes": "तीर प्रकार",
"arrowtype_sharp": "तीक्ष्ण तीर",
"arrowtype_round": "गोलाकार तीर",
@@ -182,11 +171,7 @@
"linkToElement": "वस्तु की कड़ी",
"wrapSelectionInFrame": "चौकट में चुने हुवे को लपेटे",
"tab": "",
"shapeSwitch": "आकार बदलें",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": "आकार बदलें"
},
"elementLink": {
"title": "वस्तु की कड़ी",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "त्रुटि"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "डिस्क पर सुरक्षित करे",
"disk_details": "दृश्य डेटा एक फ़ाइल में निर्यात करे, जहांसे आप उसे पुनः आयात कर सके",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "मर्मेड से एक्सकाली में",
"button": "सन्निवेश करे",
"description": "",
"description": "वर्तमान में केवल <flowchartLink>बहाव चित्र</flowchartLink>, <sequenceLink> अनुक्रम चित्र </sequenceLink> और <classLink>वर्ग चित्र</classLink> का चित्रिकरण संभव हैं. अन्य चित्र प्रकार एक्सकाली प्रतिमा जैसे चित्रित किए जायेंगे.",
"syntax": "मर्मेड विन्यास",
"preview": "पूर्वावलोकन",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Beillesztés",
"pasteAsPlaintext": "Beillesztés formázatlan szövegként",
"pasteCharts": "Grafikon beillesztése",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Összes kijelölése",
"multiSelect": "Elem hozzáadása a kijelöléshez",
"moveCanvas": "Vászon mozgatása",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "Nyíl típus",
"arrowtype_sharp": "Hegyes nyíl",
"arrowtype_round": "Ívelt nyíl",
@@ -182,11 +171,7 @@
"linkToElement": "Hivatkozás az objektumhoz",
"wrapSelectionInFrame": "",
"tab": "Tab",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "Hivatkozás az objektumhoz",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Hiba"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Mentés lemezre",
"disk_details": "Exportálja a jelenetadatokat egy fájlba, amelyből később importálhatja.",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Tempel",
"pasteAsPlaintext": "Tempel sebagai teks biasa",
"pasteCharts": "Tempel diagram",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Pilih semua",
"multiSelect": "Tambah unsur ke seleksi",
"moveCanvas": "Pindahkan kanvas",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Pilihan lain",
"cardinality": "",
"arrowtypes": "Jenis panah",
"arrowtype_sharp": "Panah runcing",
"arrowtype_round": "Panah melengkung",
@@ -182,11 +171,7 @@
"linkToElement": "Taut ke objek",
"wrapSelectionInFrame": "Bungkus pilihan dalam bingkai",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "Taut ke objek",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Kesalahan"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Simpan ke disk",
"disk_details": "Ekspor data pemandangan ke file yang mana Anda dapat impor nanti.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid menjadi Excalidraw",
"button": "Sisipkan",
"description": "",
"description": "Saat ini hanya <flowchartLink>Flowchart</flowchartLink>, <sequenceLink>Sekuen, </sequenceLink>, dan <classLink>Kelas</classLink>Diagram yang didukung. Jenis lainnya akan dirender sebagai gambar di Excalidraw.",
"syntax": "Syntax Mermaid",
"preview": "Pratinjau",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+7 -27
View File
@@ -3,10 +3,6 @@
"paste": "Incolla",
"pasteAsPlaintext": "Incolla come testo normale",
"pasteCharts": "Incolla grafici",
"chartType_bar": "Grafico a barre",
"chartType_line": "Grafico a linee",
"chartType_radar": "Grafico radar",
"chartType_plaintext": "Testo semplice",
"selectAll": "Seleziona tutto",
"multiSelect": "Aggiungi elemento alla selezione",
"moveCanvas": "Sposta tela",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Zampe di gallina (molti)",
"arrowhead_crowfoot_one": "Zampa di gallina (una)",
"arrowhead_crowfoot_one_or_many": "Zampe di gallina (una o molte)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Altre opzioni",
"cardinality": "",
"arrowtypes": "Tipo di freccia",
"arrowtype_sharp": "Freccia affilata",
"arrowtype_round": "Freccia curva",
@@ -182,11 +171,7 @@
"linkToElement": "Link all'oggetto",
"wrapSelectionInFrame": "Avvolgi la selezione nella cornice",
"tab": "Scheda",
"shapeSwitch": "Cambia forma",
"preferences": "Preferenze",
"preferences_toolLock": "Blocco strumento",
"arrowBinding": "Legatura a freccia",
"midpointSnapping": "Aggancia ai punti medi"
"shapeSwitch": "Cambia forma"
},
"elementLink": {
"title": "Link all'oggetto",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Errore"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Salva su disco",
"disk_details": "Esporta i dati della scena su file, dal quale potrai importare in seguito.",
@@ -576,9 +557,9 @@
},
"welcomeScreen": {
"app": {
"center_heading": "I tuoi disegni vengono salvati nella memoria del tuo browser.",
"center_heading_line2": "La memoria del browser potrebbe essere cancellata inaspettatamente.",
"center_heading_line3": "Salva regolarmente il tuo lavoro in un file per evitare di perderlo.",
"center_heading": "",
"center_heading_line2": "",
"center_heading_line3": "",
"center_heading_plus": "Volevi invece andare su Excalidraw+?",
"menuHint": "Esporta, preferenze, lingue, ..."
},
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Da Mermaid a Excalidraw",
"button": "Inserisci",
"description": "",
"description": "Attualmente sono supportati solo diagrammi di <flowchartLink>flusso</flowchartLink>,<sequenceLink> sequenza, </sequenceLink> e <classLink>classe </classLink>. Gli altri tipi saranno rappresentati come immagini in Excalidraw.",
"syntax": "Sintassi Mermaid",
"preview": "Anteprima",
"label": "Mermaid",
"inputPlaceholder": "Scrivi qui la definizione del diagramma Mermaid...",
"autoFixAvailable": "Correzione automatica disponibile"
"inputPlaceholder": "Scrivi qui la definizione del diagramma Mermaid..."
},
"ttd": {
"error": "Errore!"
@@ -661,7 +641,7 @@
"placeholder": {
"title": "Progettiamo il tuo diagramma",
"description": "Descrivi il diagramma che vuoi creare e noi lo genereremo per te.",
"hint": ""
"hint": "Al momento conosciamo i diagrammi di flusso, di sequenza e di classe."
},
"preview": "Anteprima",
"insert": "Inserisci",
+142 -162
View File
@@ -3,10 +3,6 @@
"paste": "貼り付け",
"pasteAsPlaintext": "書式なしテキストとして貼り付け",
"pasteCharts": "チャートの貼り付け",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "すべて選択",
"multiSelect": "複数選択",
"moveCanvas": "キャンバスを移動",
@@ -38,7 +34,7 @@
"opacity": "透明度",
"textAlign": "文字の配置",
"edges": "角",
"sharp": "シャープ",
"sharp": "四角",
"round": "丸",
"arrowheads": "線の終点",
"arrowhead_none": "なし",
@@ -50,21 +46,14 @@
"arrowhead_triangle_outline": "三角 (中抜き)",
"arrowhead_diamond": "ひし形",
"arrowhead_diamond_outline": "ひし形 (中抜き)",
"arrowhead_crowfoot_many": "カラスの足 (*)",
"arrowhead_crowfoot_one": "カラスの足 (1)",
"arrowhead_crowfoot_one_or_many": "カラスの足 (1..*)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "その他のオプション",
"cardinality": "",
"arrowtypes": "矢印タイプ",
"arrowhead_crowfoot_many": "鳥の足記法(多対多)",
"arrowhead_crowfoot_one": "鳥の足記法(一対一)",
"arrowhead_crowfoot_one_or_many": "鳥の足記法(一対多)",
"more_options": "詳細設定",
"arrowtypes": "矢印の種類",
"arrowtype_sharp": "鋭い矢印",
"arrowtype_round": "曲線矢印",
"arrowtype_elbowed": "折れ線矢印",
"arrowtype_elbowed": "ひじ矢印",
"fontSize": "フォントの大きさ",
"fontFamily": "フォントの種類",
"addWatermark": "\"Made with Excalidraw\"と表示",
@@ -86,14 +75,14 @@
"right": "右寄せ",
"extraBold": "極太",
"architect": "建築家",
"artist": "画家",
"artist": "アーティスト",
"cartoonist": "漫画家",
"fileTitle": "ファイル名",
"colorPicker": "カラーピッカー",
"canvasColors": "キャンバス上で使用",
"canvasBackground": "キャンバスの背景",
"drawingCanvas": "キャンバスの描画",
"clearCanvas": "キャンバスを消去",
"clearCanvas": "キャンバスを片付ける",
"layers": "レイヤー",
"actions": "操作",
"language": "言語",
@@ -112,7 +101,7 @@
"libraryLoadingMessage": "ライブラリを読み込み中…",
"libraries": "ライブラリを参照する",
"loadingScene": "シーンを読み込み中…",
"loadScene": "ファイルからシーンを開く",
"loadScene": "ファイルからシーン",
"align": "配置",
"alignTop": "上揃え",
"alignBottom": "下揃え",
@@ -171,22 +160,18 @@
"prompt": "プロンプト",
"followUs": "フォローする",
"discordChat": "Discord チャット",
"zoomToFitViewport": "表範囲に合わせてズーム",
"zoomToFitViewport": "表寿範囲に合わせてズーム",
"zoomToFitSelection": "選択範囲に合わせてズーム",
"zoomToFit": "すべての要素が収まるようにズーム",
"installPWA": "Excalidrawをローカルにインストール(PWA)",
"autoResize": "テキストの自動サイズ変更を有効化",
"imageCropping": "画像の切り抜き",
"unCroppedDimension": "元のサイズ",
"copyElementLink": "オブジェクトへのリンクをコピー",
"imageCropping": "画像のトリミング",
"unCroppedDimension": "",
"copyElementLink": "",
"linkToElement": "オブジェクトにリンク",
"wrapSelectionInFrame": "選択範囲をフレームで囲む",
"wrapSelectionInFrame": "選択範囲を枠で折り返す",
"tab": "Tab",
"shapeSwitch": "図形形状を変更",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": "図形形状を変更"
},
"elementLink": {
"title": "オブジェクトにリンク",
@@ -200,15 +185,15 @@
"search": {
"inputPlaceholder": "ライブラリを検索",
"heading": "ライブラリと一致",
"noResults": "一致するアイテムはありません…",
"noResults": "一致するアイテムが見つかりませんでした…",
"clearSearch": "検索のクリア"
}
},
"search": {
"title": "キャンバスで検索",
"noMatch": "一致する結果はありません…",
"singleResult": "",
"multipleResults": "",
"noMatch": "一致なし…",
"singleResult": "結果",
"multipleResults": "結果数",
"placeholder": "キャンバス内のテキストを検索…",
"frames": "フレーム",
"texts": "テキスト"
@@ -255,7 +240,7 @@
"embeddableInteractionButton": "クリックして操作"
},
"alerts": {
"clearReset": "キャンバス全体を消去します。本当によろしいですか?",
"clearReset": "この操作によってキャンバス全体が消えます。よろしいですか?",
"couldNotCreateShareableLink": "共有URLを作成できませんでした。",
"couldNotCreateShareableLinkTooBig": "共有可能なリンクを作成できませんでした: シーンが大きすぎます",
"couldNotLoadInvalidFile": "無効なファイルを読み込めませんでした。",
@@ -264,8 +249,8 @@
"couldNotCopyToClipboard": "クリップボードにコピーできませんでした。",
"decryptFailed": "データを復号できませんでした。",
"uploadedSecurly": "データのアップロードはエンドツーエンド暗号化によって保護されています。Excalidrawサーバーと第三者はデータの内容を見ることができません。",
"loadSceneOverridePrompt": "外部の描画データを読み込むと、既存のコンテンツが置き換わります。続行しますか?",
"collabStopOverridePrompt": "セッションを停止すると、ローカルに保存されている図が上書きされます。 本当によろしいですか?\n\n(ローカルの図を保持したい場合は、セッションを停止せずにブラウザーのタブを閉じてください。)",
"loadSceneOverridePrompt": "外部図面を読み込むと、既存のコンテンツが置き換わります。続行しますか?",
"collabStopOverridePrompt": "セッションを停止すると、ローカルに保存されている図が上書きされます。 本当によろしいですか?\n\n(ローカルの図を保持したい場合は、セッションを停止せずにブラウザタブを閉じてください。)",
"errorAddingToLibrary": "アイテムをライブラリに追加できませんでした",
"errorRemovingFromLibrary": "ライブラリからアイテムを削除できませんでした",
"confirmAddLibrary": "{{numShapes}} 個の図形をライブラリに追加します。よろしいですか?",
@@ -276,7 +261,7 @@
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。共同編集は無効化されています。",
"collabOfflineWarning": "インターネットに接続されていません。\n変更は保存されません!",
"localStorageQuotaExceeded": "ブラウザーのストレージ容量を超えました。変更は保存されません。"
"localStorageQuotaExceeded": ""
},
"errors": {
"unsupportedFileType": "サポートされていないファイル形式です。",
@@ -286,13 +271,13 @@
"failedToFetchImage": "画像の読み込みに失敗しました。",
"cannotResolveCollabServer": "コラボレーションサーバに接続できませんでした。ページを再読み込みして、もう一度お試しください。",
"importLibraryError": "ライブラリを読み込めませんでした。",
"saveLibraryError": "ライブラリをストレージに保存できませんでした。変更を失わないように、ローカルにファイルとして保存してください。",
"saveLibraryError": "",
"collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
"collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
"imageToolNotSupported": "画像ツールは使用不可です。",
"brave_measure_text_error": {
"line1": "<bold>Aggressly Block Fingerprinting</bold> 設定が有効なBraveブラウザを使用しているようです。",
"line2": "これにより、描画内の<bold>テキスト要素</bold>が壊れる可能性があります。",
"line1": "<bold>Aggressly Block Fingerprinting</bold> 設定が有効なBraveブラウザを使用しているようです。",
"line2": "これにより、図面の <bold>テキスト要素</bold> が壊れる可能性があります。",
"line3": "この設定を無効にすることを強く推奨します。 <link>設定手順</link> をこちらから確認できます。",
"line4": "この設定を無効にすると、テキスト要素の表示が修正されません。 GitHub で <issueLink>Issue</issueLink> を開くか、 <discordLink>Discord</discordLink> にご記入ください"
},
@@ -307,9 +292,9 @@
},
"toolBar": {
"selection": "選択",
"lasso": "なげなわ選択",
"lasso": "",
"image": "画像を挿入",
"rectangle": "長方形",
"rectangle": "形",
"diamond": "ひし形",
"ellipse": "楕円",
"arrow": "矢印",
@@ -319,31 +304,31 @@
"library": "ライブラリ",
"lock": "描画後も使用中のツールを選択したままにする",
"penMode": "ペンモード - タッチ防止",
"link": "選択した図形にリンクを追加・更新",
"link": "",
"eraser": "消しゴム",
"frame": "フレーム",
"frame": "フレームツール",
"magicframe": "ワイヤーフレームからコードを生成",
"embeddable": "Web埋め込み",
"laser": "レーザーポインター",
"hand": "手 (パンニングツール)",
"extraTools": "その他のツール",
"mermaidToExcalidraw": "Mermaid を Excalidraw に変換",
"convertElementType": "図形タイプを切り替え"
"convertElementType": ""
},
"element": {
"rectangle": "長方形",
"diamond": "ひし形",
"rectangle": "",
"diamond": "",
"ellipse": "楕円",
"arrow": "矢印",
"line": "",
"freedraw": "フリーハンド",
"arrow": "",
"line": "",
"freedraw": "",
"text": "文字",
"image": "画像",
"group": "グループ",
"frame": "フレーム",
"magicframe": "ワイヤーフレームからコードを生成",
"embeddable": "Web埋め込み",
"selection": "選択",
"embeddable": "",
"selection": "",
"iframe": "IFrame"
},
"headings": {
@@ -352,34 +337,34 @@
"shapes": "図形"
},
"hints": {
"dismissSearch": "{{shortcut}} で検索を閉じる",
"canvasPanning": "キャンバスを移動するには、{{shortcut_1}} か {{shortcut_2}} を押しながらドラッグ、または手のひらツールを使用",
"dismissSearch": "",
"canvasPanning": "",
"linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線",
"arrowTool": "クリックで複数の点を追加、ドラッグで直線。{{shortcut}} を再度押すと矢印タイプが変更されます。",
"arrowBindModifiers": "{{shortcut_1}} を押し続けるとバインドを無効化、{{shortcut_2}} を押し続けると固定点にバインド",
"arrowTool": "",
"arrowBindModifiers": "",
"freeDraw": "クリックしてドラッグします。離すと終了します",
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
"embeddable": "クリックしてドラッグし、ウェブサイトを埋め込む",
"text_selected": "ダブルクリック、または {{shortcut}} を押してテキストを編集",
"text_editing": "{{shortcut_1}} または {{shortcut_2}} を押して編集を終了",
"linearElementMulti": "最後の点をクリック、または {{shortcut_1}} か {{shortcut_2}} を押して完了",
"lockAngle": "{{shortcut}} を押し続けて角度を固定",
"resize": "{{shortcut_1}} を押し続けて縦横比を維持、\n{{shortcut_2}} を押し続けて中心からリサイズ",
"resizeImage": "{{shortcut_1}} を押し続けて自由にリサイズ、\n{{shortcut_2}} を押し続けて中心からリサイズ",
"rotate": "{{shortcut}} を押し続けて角度を固定して回転",
"lineEditor_info": "{{shortcut_1}} を押しながらダブルクリック、または {{shortcut_2}} を押して点を編集",
"lineEditor_line_info": "ダブルクリック、または {{shortcut}} を押して点を編集",
"lineEditor_pointSelected": "{{shortcut_1}} で点を削除、\n{{shortcut_2}} で複製、またはドラッグで移動",
"lineEditor_nothingSelected": "点を選択して編集 ({{shortcut_1}} を押し続けて複数選択)、\nまたは {{shortcut_2}} を押しながらクリックで新しい点を追加",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
"lockAngle": "",
"resize": "",
"resizeImage": "",
"rotate": "",
"lineEditor_info": "",
"lineEditor_line_info": "",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"publishLibrary": "自分のライブラリを公開",
"bindTextToElement": "{{shortcut}} でテキストを追加",
"createFlowchart": "{{shortcut}} でフローチャートを作成",
"deepBoxSelect": "{{shortcut}} を押し続けて詳細選択・ドラッグ防止",
"eraserRevert": "{{shortcut}} を押し続けて削除マークを取り消し",
"bindTextToElement": "",
"createFlowchart": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": "この機能は、\"dom.events.asyncClipboard.clipboardItem\" フラグを \"true\" に設定することで有効になる可能性があります。Firefox でブラウザーの設定を変更するには、\"about:config\" ページを参照してください。",
"disableSnapping": "{{shortcut}} を押し続けてスナップを無効化",
"enterCropEditor": "画像をダブルクリック、または {{shortcut}} を押して切り抜き",
"leaveCropEditor": "画像の外をクリック、または {{shortcut_1}} か {{shortcut_2}} を押して切り抜きを終了"
"disableSnapping": "",
"enterCropEditor": "",
"leaveCropEditor": ""
},
"canvasError": {
"cannotShowPreview": "プレビューを表示できません",
@@ -388,17 +373,17 @@
},
"errorSplash": {
"headingMain": "エラーが発生しました。もう一度やり直してください。 <button>ページを再読み込みする。</button>",
"clearCanvasMessage": "再読み込みがうまくいかない場合は、<button>キャンバスを消去</button>してみてください。",
"clearCanvasMessage": "再読み込みがうまくいかない場合は、 <button>キャンバスを消去しています</button>",
"clearCanvasCaveat": " これにより作業が失われます ",
"trackedToSentry": "エラー (識別子: {{eventId}}) はシステムに記録されました。",
"openIssueMessage": "エラーにシーン情報が含まれないよう配慮しています。シーンが非公開でなければ、<button>バグ報告</button>へのご協力をお願いします。以下の情報をコピーしてGitHub Issueに貼り付けてください。",
"trackedToSentry": "識別子のエラー {{eventId}} が我々のシステムで追跡されました。",
"openIssueMessage": "エラーに関するシーン情報を含めないように非常に慎重に設定しました。もしあなたのシーンがプライベートでない場合は、私たちのフォローアップを検討してください。 <button>バグ報告</button> GitHub Issueに以下の情報をコピーして貼り付けてください。",
"sceneContent": "シーンの内容:"
},
"shareDialog": {
"or": "または"
},
"roomDialog": {
"desc_intro": "他の人を描画の共同編集に招待しましょう。",
"desc_intro": "図面にコラボレーションするよう人々を招待しま。",
"desc_privacy": "心配しないでください、セッションはエンドツーエンドで暗号化されており、完全にプライベートです。私たちのサーバーでさえも、あなたが描いたものを見ることができません。",
"button_startSession": "セッションを開始する",
"button_stopSession": "セッションを終了する",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "エラー"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "ディスクに保存",
"disk_details": "シーンデータを後からインポートできるファイルにエクスポートします。",
@@ -430,8 +411,8 @@
"click": "クリック",
"deepSelect": "深い選択",
"deepBoxSelect": "ボックス内の深い選択、およびドラッグの抑止",
"createFlowchart": "汎用要素からフローチャートを作成",
"navigateFlowchart": "フローチャート内を移動",
"createFlowchart": "",
"navigateFlowchart": "",
"curvedArrow": "カーブした矢印",
"curvedLine": "曲線",
"documentation": "ドキュメント",
@@ -455,8 +436,8 @@
"toggleElementLock": "選択したアイテムをロック/ロック解除",
"movePageUpDown": "ページを上下に移動",
"movePageLeftRight": "ページを左右に移動",
"cropStart": "画像を切り抜き",
"cropFinish": "画像の切り抜きを終了"
"cropStart": "",
"cropFinish": ""
},
"clearCanvasDialog": {
"title": "キャンバスを消去"
@@ -482,10 +463,10 @@
"required": "必須項目",
"website": "有効な URL を入力してください"
},
"noteDescription": "あなたのライブラリを<link>公開ライブラリリポジトリ</link>に投稿して、他の人が作図に使えるようにしましょう。",
"noteGuidelines": "投稿されたライブラリは担当者が審査します。投稿前に<link>ガイドライン</link>をお読みください。修正をお願いすることがあるため、GitHubアカウントがあると便利ですが、必須ではありません。",
"noteLicense": "投稿すること、ライブラリが<link>MITライセンス</link>で公開されることに同意したものとみなします。これは誰でも自由に利用できることを意味します。",
"noteItems": "各アイテムにはフィルタリング用に固有の名前が必要です。以下のアイテムが含まれます:",
"noteDescription": "以下に含めるライブラリを提出してください <link>公開ライブラリリポジトリ</link>他の人が作図に使えるようにするためです",
"noteGuidelines": "最初にライブラリを手動で承認する必要があります。次をお読みください <link>ガイドライン</link> 送信する前に、GitHubアカウントが必要になりますが、必須ではありません。",
"noteLicense": "提出することにより、ライブラリが次の下で公開されることに同意します: <link>MIT ライセンス </link>つまり誰でも制限なく使えるということです",
"noteItems": "各ライブラリ項目は、フィルタリングのために独自の名前を持つ必要があります。以下のライブラリアイテムが含まれます:",
"atleastOneLibItem": "開始するには少なくとも1つのライブラリ項目を選択してください",
"republishWarning": "注意: 選択された項目の中には、すでに公開/投稿済みと表示されているものがあります。既存のライブラリや投稿を更新する場合のみ、アイテムを再投稿してください。"
},
@@ -527,15 +508,15 @@
},
"stats": {
"angle": "角度",
"shapes": "図形",
"shapes": "",
"height": "高さ",
"scene": "シーン",
"selected": "選択済み",
"storage": "ストレージ",
"fullTitle": "キャンバス・図形のプロパティ",
"fullTitle": "",
"title": "プロパティ",
"generalStats": "全般",
"elementProperties": "図形のプロパティ",
"generalStats": "",
"elementProperties": "",
"total": "合計",
"version": "バージョン",
"versionCopy": "クリックしてコピー",
@@ -547,7 +528,7 @@
"copyStyles": "スタイルをコピーしました。",
"copyToClipboard": "クリップボードにコピー",
"copyToClipboardAsPng": "{{exportSelection}} を PNG 形式でクリップボードにコピーしました\n({{exportColorScheme}})",
"copyToClipboardAsSvg": "{{exportSelection}} を SVG 形式でクリップボードにコピーしました\n({{exportColorScheme}})",
"copyToClipboardAsSvg": "",
"fileSaved": "ファイルを保存しました",
"fileSavedToFilename": "{filename} に保存しました",
"canvas": "キャンバス",
@@ -555,7 +536,7 @@
"pasteAsSingleElement": "{{shortcut}} を使用して単一の要素として貼り付けるか、\n既存のテキストエディタに貼り付け",
"unableToEmbed": "この URL の埋め込みは現在許可されていません。URL のホワイトリストへの追加をリクエストするには、GitHub で Issue を上げてください。",
"unrecognizedLinkFormat": "埋め込もうとしたリンクは期待するフォーマットと一致しません。埋め込み元のサイトで提供される「embed」の文字列を貼り付けてください。",
"elementLinkCopied": "リンクをクリップボードにコピーしました"
"elementLinkCopied": ""
},
"colors": {
"transparent": "透明",
@@ -576,9 +557,9 @@
},
"welcomeScreen": {
"app": {
"center_heading": "描画データはブラウザーのストレージに保存されます。",
"center_heading_line2": "ブラウザーのストレージは予期せず消去されることがあります。",
"center_heading_line3": "作業を失わないよう、定期的にファイルへ保存してください。",
"center_heading": "",
"center_heading_line2": "",
"center_heading_line3": "",
"center_heading_plus": "代わりにExcalidraw+を開きますか?",
"menuHint": "エクスポート、設定、言語..."
},
@@ -590,7 +571,7 @@
}
},
"colorPicker": {
"color": "",
"color": "",
"mostUsedCustomColors": "最も使用されているカスタム色",
"colors": "色",
"shades": "影",
@@ -619,77 +600,76 @@
"loadFromFile": {
"title": "ファイルからロード",
"button": "ファイルからロード",
"description": "ファイルからの読み込みは、<bold>現在の描画内容を置き換えます</bold>。<br></br>その前に、以下の選択肢のいずれかにより描画内容を保存できます。"
"description": "ファイルからのロードは、<bold>現在の描画内容を置き換えます</bold>。<br></br>その前に、以下の選択肢のいずれかにより描画内容を保存できます。"
},
"shareableLink": {
"title": "リンクから読み込み",
"title": "リンクからロード",
"button": "描画内容を置き換える",
"description": "外部の描画データの読み込みは、<bold>現在の描画内容を置き換えます</bold>。<br></br>その前に、以下の選択肢のいずれかにより描画内容を保存できます。"
"description": "外部図面のロードは、<bold>現在の描画内容を置き換えます</bold>。<br></br>その前に、以下の選択肢のいずれかにより描画内容を保存できます。"
}
}
},
"mermaid": {
"title": "Mermaid を Excalidraw に変換",
"button": "挿入",
"description": "",
"description": "現在、<flowchartLink>Flowchart</flowchartLink>、<sequenceLink>Sequence</sequenceLink>、<classLink>Class</classLink> のダイアグラムのみに対応しています。その他の種類は、Excalidraw では画像として描画されます。",
"syntax": "Mermaid 構文",
"preview": "プレビュー",
"label": "Mermaid",
"inputPlaceholder": "Mermaid のダイアグラム定義をここに入力...",
"autoFixAvailable": ""
"label": "",
"inputPlaceholder": ""
},
"ttd": {
"error": "エラー!"
"error": ""
},
"chat": {
"inputPlaceholder": "ダイアグラムのアイデアを入力... ({{shortcut}} で改行)",
"inputPlaceholderWithMessages": "ダイアグラムを修正...",
"generating": "生成中...",
"rateLimitRemaining": "本日の残りリクエスト回数: {{count}}",
"inputPlaceholder": "",
"inputPlaceholderWithMessages": "",
"generating": "",
"rateLimitRemaining": "",
"role": {
"user": "あなた",
"assistant": "AIアシスタント",
"system": "システム"
"user": "",
"assistant": "",
"system": ""
},
"aiBeta": "AI ベータ版",
"label": "チャット",
"menu": "メニュー",
"newChat": "新しいチャット",
"deleteChat": "チャットを削除",
"deleteMessage": "メッセージを削除",
"viewAsMermaid": "Mermaidで表示",
"aiBeta": "",
"label": "",
"menu": "",
"newChat": "",
"deleteChat": "",
"deleteMessage": "",
"viewAsMermaid": "",
"placeholder": {
"title": "ダイアグラムをデザインしよう",
"description": "作りたいダイアグラムを説明してください。AIが生成します。",
"title": "",
"description": "",
"hint": ""
},
"preview": "プレビュー",
"insert": "挿入",
"retry": "再試行",
"preview": "",
"insert": "",
"retry": "",
"errors": {
"promptTooShort": "プロンプトが短すぎます ({{min}} 文字以上)",
"promptTooLong": "プロンプトが長すぎます ({{max}} 文字以下)",
"generationFailed": "生成に失敗しました",
"invalidDiagram": "無効なダイアグラムが生成されました。手動で編集、自動修正で再試行、または別のプロンプトをお試しください。",
"fixInMermaid": "Mermaidで手動編集 →",
"aiRepair": "再生成 (自動修正) →",
"requestAborted": "リクエストが中断されました",
"requestFailed": "リクエストに失敗しました",
"mermaidParseError": "Mermaid構文エラー"
"promptTooShort": "",
"promptTooLong": "",
"generationFailed": "",
"invalidDiagram": "",
"fixInMermaid": "",
"aiRepair": "",
"requestAborted": "",
"requestFailed": "",
"mermaidParseError": ""
},
"rateLimit": {
"messageLimit": "無料プランのAI利用上限に達しました。Excalidraw+をお試しいただくか、また明日ご利用ください。",
"generalRateLimit": "操作が速すぎたようです。もう少し待ってから再度お試しください。",
"messageLimitInputPlaceholder": "メッセージの送信上限に達しました"
"messageLimit": "",
"generalRateLimit": "",
"messageLimitInputPlaceholder": ""
},
"upsellBtnLabel": "Plusにアップグレード"
"upsellBtnLabel": ""
},
"quickSearch": {
"placeholder": "クイック検索"
"placeholder": ""
},
"fontList": {
"badge": {
"old": ""
"old": ""
},
"sceneFonts": "このシーン内",
"availableFonts": "利用可能フォント",
@@ -700,36 +680,36 @@
"hint": {
"text": "ユーザーをクリックしてフォロー",
"followStatus": "現在このユーザーをフォローしています",
"inCall": "ユーザーは音声通話中です",
"micMuted": "ユーザーのマイクはミュート中です",
"isSpeaking": "ユーザーは発話中です"
"inCall": "",
"micMuted": "",
"isSpeaking": ""
}
},
"commandPalette": {
"title": "コマンドパレット",
"title": "",
"shortcuts": {
"select": "選択",
"confirm": "確認",
"close": "閉じる"
},
"recents": "最近使ったもの",
"recents": "",
"search": {
"placeholder": "メニュー、コマンドを検索して便利な機能を見つけよう",
"noMatch": "一致するコマンドはありません..."
"placeholder": "",
"noMatch": ""
},
"itemNotAvailable": "コマンドを利用できません...",
"itemNotAvailable": "",
"shortcutHint": "コマンドパレットには{{shortcut}}を使用"
},
"keys": {
"ctrl": "Ctrl",
"option": "Option",
"cmd": "Cmd",
"alt": "Alt",
"escape": "Esc",
"enter": "Enter",
"shift": "Shift",
"spacebar": "スペース",
"delete": "Delete",
"mmb": "スクロールホイール"
"ctrl": "",
"option": "",
"cmd": "",
"alt": "",
"escape": "",
"enter": "",
"shift": "",
"spacebar": "",
"delete": "",
"mmb": ""
}
}
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Qoyıw",
"pasteAsPlaintext": "Ápiwayı tekst retinde qoyıw",
"pasteCharts": "Diagrammalardı qoyıw",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Barlıǵın tańlaw",
"multiSelect": "",
"moveCanvas": "",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Qátelik"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Diskke saqlaw",
"disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Senṭeḍ",
"pasteAsPlaintext": "",
"pasteCharts": "Senṭeḍ udlifen",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Fren akk",
"multiSelect": "Rnu aferdis ɣer tefrayt",
"moveCanvas": "Smutti taɣzut n usuneɣ",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Tuccḍa"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Sekles deg uḍebsi",
"disk_details": "Sekles isefka n usayes deg ufaylu ansi ara tizmireḍ ad d-tketreḍ areḍqal.",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Қою",
"pasteAsPlaintext": "",
"pasteCharts": "Диаграммаларды қою",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Бәрін таңдау",
"multiSelect": "",
"moveCanvas": "",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Қате"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "",
"disk_details": "Сахна деректерін кейін қайта импорттауға болатын файлға экспорттаңыз.",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "បិទភ្ជាប់",
"pasteAsPlaintext": "បិទភ្ជាប់ជាអត្ថបទធម្មតា",
"pasteCharts": "បិទភ្ជាប់តារាង",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "ជ្រើសរើស​ទាំងអស់",
"multiSelect": "បន្ថែមធាតុទៅលើការជ្រើសរើស",
"moveCanvas": "ផ្លាស់ទីបាវ",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "មានកំហុស"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "រក្សាទុកទៅថាស",
"disk_details": "នាំចេញទិន្នន័យរបស់ស៊ីនជាឯកសារដែលអ្នកអាចនាំចូលនៅពេលក្រោយ។",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "붙여넣기",
"pasteAsPlaintext": "일반 텍스트로 붙여넣기",
"pasteCharts": "차트 붙여넣기",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "전체 선택",
"multiSelect": "선택 영역에 추가하기",
"moveCanvas": "캔버스 이동",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "화살표 모양",
"arrowtype_sharp": "뾰족한 화살표",
"arrowtype_round": "곡선 화살표",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "오류"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "디스크에 저장",
"disk_details": "나중에 다시 불러올 수 있도록 화면 데이터를 내보냅니다.",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "دانانەوە",
"pasteAsPlaintext": "دایبنێ وەک دەقی سادە",
"pasteCharts": "دانانەوەی خشتەکان",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "دیاریکردنی هەموو",
"multiSelect": "زیادکردنی بۆ دیاریکراوەکان",
"moveCanvas": "تابلۆ بجوڵێنە",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "هه‌ڵه‌ ڕوویدا"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "پاشەکەوت بکە لە دیسک",
"disk_details": "هەناردەکردنی داتای دیمەنەکە بۆ فایلێک کە دواتر دەتوانیت لێی هاوردە بکەیت.",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Įklijuoti",
"pasteAsPlaintext": "Įklijuoti kaip paprastą tekstą",
"pasteCharts": "Įklijuoti diagramas",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Pažymėti viską",
"multiSelect": "Pridėkite elementą prie pasirinktų",
"moveCanvas": "Judinti drobę",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Klaida"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Įrašyti į diską",
"disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Ielīmēt",
"pasteAsPlaintext": "Ielīmēt kā vienkāršu tekstu",
"pasteCharts": "Ielīmēt grafikus",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Atlasīt visu",
"multiSelect": "Pievienot elementu atlasei",
"moveCanvas": "Pārvietot tāfeli",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Kļūda"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Saglabāt diskā",
"disk_details": "Eksportēt ainas datus datnē, ko vēlāk varēsiet importēt.",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "चिटकवा",
"pasteAsPlaintext": "साधा मजकूर च्या रुपात पेस्ट करा",
"pasteCharts": "चार्ट चिकटवा",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "समस्त निवडा",
"multiSelect": "निवडित तत्व जोडा",
"moveCanvas": "पटल हलवा",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "चिमणि पाय (अनेक)",
"arrowhead_crowfoot_one": "चिमणि पाय (एक)",
"arrowhead_crowfoot_one_or_many": "चिमणि पाय (एक अथवा अनेक)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "आणिक विकल्प",
"cardinality": "",
"arrowtypes": "बाणाचे प्रकार",
"arrowtype_sharp": "तीक्ष्ण तीर",
"arrowtype_round": "वक्राकार तीर",
@@ -182,11 +171,7 @@
"linkToElement": "वस्तू ह्याचा दुवा",
"wrapSelectionInFrame": "चौकटित निवडलेले गुंडाळा",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "वस्तू ह्याचा दुवा",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "त्रुटि"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "डिस्क मधे जतन करा",
"disk_details": "सीन डेटा बाहेर एक फ़ाइल मधे जतन करा, त्या फ़ाइल मधुम तो डेटा नंतर परत आणु शकता.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "मर्मेड पासून एक्सकाली मधे",
"button": "शिरवा",
"description": "",
"description": "सध्या फक्त <flowchartLink> प्रवाह चित्र (फ़्लो चार्ट) </flowchartLink> आणि <sequenceLink> क्रम चित्र (सिकवेंस ड़ायग्राम) </sequenceLink> करता येतात. बाक़ीचे चित्र प्रकार एक्सकाली चित्र पद्धति नी चित्रित होतील.",
"syntax": "मर्मेड संरचना नियम",
"preview": "पूर्वावलोकन",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "ထား",
"pasteAsPlaintext": "",
"pasteCharts": "",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "အကုန်ရွေး",
"multiSelect": "ရွေးထားသည့်ထဲပုံထည့်",
"moveCanvas": "ကားချပ်ရွှေ့",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "ချို့ယွင်းချက်"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Lim inn",
"pasteAsPlaintext": "Lim inn som klartekst",
"pasteCharts": "Lim inn diagrammer",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Velg alt",
"multiSelect": "Legg til element i utvalg",
"moveCanvas": "Flytt lerretet",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Kråkefot (mange)",
"arrowhead_crowfoot_one": "Kråkefot (én)",
"arrowhead_crowfoot_one_or_many": "Kråkefot (én eller mange)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Flere alternativer",
"cardinality": "",
"arrowtypes": "Type pil",
"arrowtype_sharp": "Skarp pil",
"arrowtype_round": "Buet pil",
@@ -182,11 +171,7 @@
"linkToElement": "Lenke til objekt",
"wrapSelectionInFrame": "Brekk om utvalg i ramme",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "Lenke til objekt",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Feil"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Lagre til disk",
"disk_details": "Eksporter scene-dataene til en fil som du kan importere fra senere.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid til Excalidraw",
"button": "Sett inn",
"description": "",
"description": "Foreløpig er bare <flowchartLink>Flowchart</flowchartLink>-,<sequenceLink> Sequence</sequenceLink>- og <classLink>klasse </classLink>-diagrammer støttet. De andre typene vil bli gjengitt som bilde i Excalidraw.",
"syntax": "Mermaid-syntaks",
"preview": "Forhåndsvisning",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+9 -29
View File
@@ -3,10 +3,6 @@
"paste": "Plakken",
"pasteAsPlaintext": "Plakken als platte tekst",
"pasteCharts": "Grafieken plakken",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Alles selecteren",
"multiSelect": "Voeg element toe aan selectie",
"moveCanvas": "Canvas verplaatsen",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Kraaienpoot (veel)",
"arrowhead_crowfoot_one": "Kraaienpoot (enkel)",
"arrowhead_crowfoot_one_or_many": "Kraaienpoot (enkel of veel)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Meer opties",
"cardinality": "",
"arrowtypes": "Pijl type",
"arrowtype_sharp": "Scherpe pijl",
"arrowtype_round": "Gebogen pijl",
@@ -182,11 +171,7 @@
"linkToElement": "Link naar object",
"wrapSelectionInFrame": "Selectie omslaan in frame",
"tab": "Tab",
"shapeSwitch": "Verander vorm",
"preferences": "Voorkeuren",
"preferences_toolLock": "Tool vergrendelen",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": "Verander vorm"
},
"elementLink": {
"title": "Link naar object",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Fout"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Opslaan op schijf",
"disk_details": "De scènegegevens exporteren naar een bestand waaruit u later kunt importeren.",
@@ -576,9 +557,9 @@
},
"welcomeScreen": {
"app": {
"center_heading": "Je tekeningen worden bewaard in de opslag van je browser.",
"center_heading_line2": "Browser opslag kan onverwacht gewist worden.",
"center_heading_line3": "Sla je werk regelmatig op in een bestand om te voorkomen dat je het kwijtraakt.",
"center_heading": "",
"center_heading_line2": "",
"center_heading_line3": "",
"center_heading_plus": "Wil je in plaats daarvan naar Excalidraw+ gaan?",
"menuHint": "Exporteren, voorkeuren en meer, ..."
},
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid naar Excalidraw",
"button": "Invoegen",
"description": "",
"description": "Momenteel worden alleen <flowchartLink>Flowchart</flowchartLink>-, <sequenceLink>Sequence</sequenceLink>- en <classLink>Class</classLink>-diagrammen ondersteund. De andere types worden als afbeelding weergegeven in Excalidraw.",
"syntax": "Mermaid Syntaxis",
"preview": "Voorbeeld",
"label": "Mermaid",
"inputPlaceholder": "Noteer Mermaid diagram omschrijving hier...",
"autoFixAvailable": ""
"inputPlaceholder": "Noteer Mermaid diagram omschrijving hier..."
},
"ttd": {
"error": "Fout!"
@@ -644,7 +624,7 @@
"chat": {
"inputPlaceholder": "Begin je diagram idee hier in te typen... ({{shortcut}} voor een nieuwe lijn)",
"inputPlaceholderWithMessages": "Ga door met het verfijnen van je diagram...",
"generating": "Genereren...",
"generating": "",
"rateLimitRemaining": "{{count}} verzoeken over vandaag",
"role": {
"user": "Jij",
@@ -661,7 +641,7 @@
"placeholder": {
"title": "Laten we jouw diagram ontwerpen",
"description": "Beschrijf het diagram dat je wilt aanmaken, en we genereren het voor je.",
"hint": ""
"hint": "Op dit moment kennen we Flowchart, Sequence, en Class diagrammen."
},
"preview": "Voorbeeld",
"insert": "Invoegen",
@@ -675,7 +655,7 @@
"aiRepair": "Opnieuw genereren (automatisch repareren) →",
"requestAborted": "Verzoek gestopt",
"requestFailed": "Verzoek mislukt",
"mermaidParseError": "Mermaid syntax fout"
"mermaidParseError": ""
},
"rateLimit": {
"messageLimit": "Je hebt je AI limiet bereikt op het gratis abonnement. Probeer Excalidraw+ uit voor meer of kom morgen terug.",
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Lim inn",
"pasteAsPlaintext": "",
"pasteCharts": "Lim inn diagram",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Vel alt",
"multiSelect": "Legg til element i utval",
"moveCanvas": "Flytt lerretet",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Feil"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Lagre til disk",
"disk_details": "Eksporter scenedataa til ei fil du kan importere seinare.",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Pegar",
"pasteAsPlaintext": "Pegar en tèxt brut",
"pasteCharts": "Pegar los grafics",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Tot seleccionar",
"multiSelect": "Apondre un element a la seleccion",
"moveCanvas": "Desplaçar lo canabàs",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "Ligam cap a lobjècte",
"wrapSelectionInFrame": "",
"tab": "Onglet",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Error"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Salvar al disc",
"disk_details": "Exportar las donadas de la scèna cap a un fichièr que podètz importar mai tard.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "De Mermaid cap a Excalidraw",
"button": "Inserir",
"description": "",
"description": "Actualament, sonque los diagramas <flowchartLink>logics</flowchartLink>,<sequenceLink> de sequéncia</sequenceLink> e <classLink>de classa </classLink>son preses en carga. Los autres tipes seràn afichats coma imatge dins Excalidraw.",
"syntax": "Sintaxi Mermaid",
"preview": "Apercebut",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "ਪੇਸਟ ਕਰੋ",
"pasteAsPlaintext": "ਸਾਦੇ ਪਾਠ ਵਜੋਂ ਪੇਸਟ ਕਰੋ",
"pasteCharts": "ਚਾਰਟ ਪੇਸਟ ਕਰੋ",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "ਸਾਰੇ ਚੁਣੋ",
"multiSelect": "ਐਲੀਮੈਂਟ ਨੂੰ ਚੋਣ ਵਿੱਚ ਜੋੜੋ",
"moveCanvas": "ਕੈਨਵਸ ਹਿਲਾਓ",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "",
"cardinality": "",
"arrowtypes": "",
"arrowtype_sharp": "",
"arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "",
"wrapSelectionInFrame": "",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "ਗਲਤੀ"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "ਡਿਸਕ ਵਿੱਚ ਸਾਂਭੋ",
"disk_details": "ਦ੍ਰਿਸ਼ ਦਾ ਡਾਟਾ ਫਾਈਲ ਵਿੱਚ ਨਿਰਯਾਤ ਕਰੋ ਜਿੱਥੋਂ ਤੁਸੀਂ ਇਸਨੂੰ ਬਾਅਦ ਵਿੱਚ ਆਯਾਤ ਕਰ ਸਕਦੇ ਹੋ।",
@@ -635,8 +616,7 @@
"syntax": "",
"preview": "",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+52 -52
View File
@@ -1,59 +1,59 @@
{
"ar-SA": 82,
"az-AZ": 24,
"bg-BG": 63,
"bn-BD": 38,
"bn-IN": 38,
"ca-ES": 82,
"cs-CZ": 74,
"ar-SA": 84,
"az-AZ": 25,
"bg-BG": 65,
"bn-BD": 39,
"bn-IN": 39,
"ca-ES": 84,
"cs-CZ": 76,
"da-DK": 28,
"de-CH": 83,
"de-DE": 83,
"el-GR": 77,
"de-CH": 85,
"de-DE": 85,
"el-GR": 80,
"en": 100,
"es-ES": 82,
"eu-ES": 70,
"fa-IR": 83,
"fi-FI": 60,
"fr-FR": 97,
"gl-ES": 61,
"he-IL": 82,
"hi-IN": 83,
"hu-HU": 65,
"id-ID": 81,
"it-IT": 98,
"ja-JP": 96,
"kaa": 21,
"kab-KAB": 54,
"es-ES": 85,
"eu-ES": 72,
"fa-IR": 84,
"fi-FI": 62,
"fr-FR": 92,
"gl-ES": 63,
"he-IL": 84,
"hi-IN": 86,
"hu-HU": 67,
"id-ID": 84,
"it-IT": 99,
"ja-JP": 81,
"kaa": 22,
"kab-KAB": 56,
"kk-KZ": 13,
"km-KH": 54,
"ko-KR": 73,
"ku-TR": 58,
"lt-LT": 33,
"lv-LV": 51,
"mr-IN": 82,
"my-MM": 24,
"nb-NO": 82,
"nl-NL": 96,
"nn-NO": 43,
"oc-FR": 76,
"pa-IN": 52,
"pl-PL": 90,
"pt-BR": 86,
"pt-PT": 82,
"ro-RO": 98,
"ru-RU": 98,
"si-LK": 67,
"sk-SK": 90,
"sl-SI": 82,
"sv-SE": 82,
"ta-IN": 78,
"th-TH": 65,
"tr-TR": 92,
"uk-UA": 86,
"km-KH": 56,
"ko-KR": 75,
"ku-TR": 59,
"lt-LT": 35,
"lv-LV": 53,
"mr-IN": 84,
"my-MM": 25,
"nb-NO": 84,
"nl-NL": 99,
"nn-NO": 45,
"oc-FR": 79,
"pa-IN": 54,
"pl-PL": 93,
"pt-BR": 84,
"pt-PT": 84,
"ro-RO": 100,
"ru-RU": 99,
"si-LK": 70,
"sk-SK": 93,
"sl-SI": 84,
"sv-SE": 85,
"ta-IN": 81,
"th-TH": 59,
"tr-TR": 88,
"uk-UA": 84,
"uz-UZ": 0,
"vi-VN": 68,
"zh-CN": 91,
"vi-VN": 70,
"zh-CN": 93,
"zh-HK": 16,
"zh-TW": 98
"zh-TW": 84
}
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Wklej",
"pasteAsPlaintext": "Wklej jako zwykły tekst",
"pasteCharts": "Wklej wykresy",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Zaznacz wszystko",
"multiSelect": "Dodaj element do zaznaczenia",
"moveCanvas": "Przesuń obszar roboczy",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Notacja Martina (wiele)",
"arrowhead_crowfoot_one": "Notacja Martina (jeden)",
"arrowhead_crowfoot_one_or_many": "Notacja Martina (jeden lub wiele)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Więcej opcji",
"cardinality": "",
"arrowtypes": "Typ strzałki",
"arrowtype_sharp": "Ostra strzałka",
"arrowtype_round": "Zaokrąglona strzałka",
@@ -182,11 +171,7 @@
"linkToElement": "Link do obiektu",
"wrapSelectionInFrame": "Owiń wybór w ramkę",
"tab": "Tab",
"shapeSwitch": "Wybór kształtu",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": "Wybór kształtu"
},
"elementLink": {
"title": "Link do obiektu",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Wystąpił błąd"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Zapisz na dysku",
"disk_details": "Eksportuj dane sceny do pliku, z którego możesz importować później.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Konwertuj diagram Mermaid do Excalidraw",
"button": "Wstaw",
"description": "",
"description": "Obecnie wspierane są jedynie <flowchartLink>proste grafy</flowchartLink>, <sequenceLink>sekwencje</sequenceLink> i <classLink>diagramy klas</classLink>. Pozostałe typy będą wyświetlane jako obrazy w Excalidraw.",
"syntax": "Składnia diagramów Mermaid",
"preview": "Podgląd",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""
+26 -46
View File
@@ -3,10 +3,6 @@
"paste": "Colar",
"pasteAsPlaintext": "Colar como texto sem formatação",
"pasteCharts": "Colar gráficos",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Selecionar tudo",
"multiSelect": "Adicionar elemento à seleção",
"moveCanvas": "Mover tela",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Pé de pássaro (muitos)",
"arrowhead_crowfoot_one": "Pé de pássaro (um)",
"arrowhead_crowfoot_one_or_many": "Pé de pássaro (um ou muitos)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Mais opções",
"cardinality": "",
"arrowtypes": "Tipo de seta",
"arrowtype_sharp": "Seta afiada",
"arrowtype_round": "Seta curva",
@@ -146,15 +135,15 @@
"labelEmbed": "Vincular e incorporar",
"empty": "Nenhum link foi definido",
"hint": "Digite ou cole o seu link aqui",
"goToElement": "Ir para o elemento alvo"
"goToElement": ""
},
"lineEditor": {
"edit": "Editar linha",
"editArrow": "Editar seta"
},
"polygon": {
"breakPolygon": "Parar polígono",
"convertToPolygon": "Converter para polígono"
"breakPolygon": "",
"convertToPolygon": ""
},
"elementLock": {
"lock": "Bloquear",
@@ -181,15 +170,11 @@
"copyElementLink": "Copiar link para o objeto",
"linkToElement": "Links para o objeto",
"wrapSelectionInFrame": "Ajustar seleção no frame",
"tab": "Aba",
"shapeSwitch": "Trocar forma",
"preferences": "Preferências",
"preferences_toolLock": "Bloqueio de ferramenta",
"arrowBinding": "",
"midpointSnapping": ""
"tab": "",
"shapeSwitch": ""
},
"elementLink": {
"title": "Links para o objeto",
"title": "",
"desc": "Clique em uma forma na tela ou cole um link.",
"notFound": "Objeto vinculado não foi encontrado na tela."
},
@@ -199,9 +184,9 @@
"hint_emptyPrivateLibrary": "Selecione um item na tela para adicioná-lo aqui.",
"search": {
"inputPlaceholder": "",
"heading": "Correspondências da biblioteca",
"noResults": "Nenhum resultado encontrado...",
"clearSearch": "Limpar pesquisa"
"heading": "",
"noResults": "",
"clearSearch": ""
}
},
"search": {
@@ -210,8 +195,8 @@
"singleResult": "resultado",
"multipleResults": "resultados",
"placeholder": "Procurar texto na tela...",
"frames": "Quadros",
"texts": "Textos"
"frames": "",
"texts": ""
},
"buttons": {
"clearReset": "Limpar a tela",
@@ -245,7 +230,7 @@
"objectsSnapMode": "Encaixar em objetos",
"exitZenMode": "Sair do modo zen",
"cancel": "Cancelar",
"saveLibNames": "Salvar nome(s) e sair",
"saveLibNames": "",
"clear": "Limpar",
"remove": "Remover",
"embed": "Alternar incorporação",
@@ -276,7 +261,7 @@
"removeItemsFromsLibrary": "Excluir {{count}} item(ns) da biblioteca?",
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada.",
"collabOfflineWarning": "Sem conexão com a internet disponível.\nSuas alterações não serão salvas!",
"localStorageQuotaExceeded": "A cota de armazenamento do navegador excedida. As alterações não serão salvas."
"localStorageQuotaExceeded": ""
},
"errors": {
"unsupportedFileType": "Tipo de arquivo não suportado.",
@@ -328,7 +313,7 @@
"hand": "Mão (ferramenta de rolagem)",
"extraTools": "Mais ferramentas",
"mermaidToExcalidraw": "Mermaid para Excalidraw",
"convertElementType": "Alternar tipo de forma"
"convertElementType": ""
},
"element": {
"rectangle": "Retângulo",
@@ -352,7 +337,7 @@
"shapes": "Formas"
},
"hints": {
"dismissSearch": "{{shortcut}} para descartar a pesquisa",
"dismissSearch": "",
"canvasPanning": "",
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
"arrowTool": "",
@@ -372,8 +357,8 @@
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"publishLibrary": "Publicar sua própria biblioteca",
"bindTextToElement": "{{shortcut}} para adicionar texto",
"createFlowchart": "{{shortcut}} para criar um fluxo",
"bindTextToElement": "",
"createFlowchart": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": "Esse recurso pode ser ativado configurando a opção \"dom.events.asyncClipboard.clipboardItem\" como \"true\". Para alterar os sinalizadores do navegador no Firefox, visite a página \"about:config\".",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Erro"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Salvar no computador",
"disk_details": "Exportar os dados da cena para um arquivo que você poderá importar mais tarde.",
@@ -555,7 +536,7 @@
"pasteAsSingleElement": "Use {{shortcut}} para colar como um único elemento,\nou cole em um editor de texto já existente",
"unableToEmbed": "No momento não é permitido incorporar esta URL. Crie uma issue no GitHub para solicitar a lista branca da URL",
"unrecognizedLinkFormat": "O link incorporado não corresponde ao formato esperado. Por favor, tente colar a string 'incorporada' que foi fornecida pelo site de origem",
"elementLinkCopied": "Link copiado para a área de transferência"
"elementLinkCopied": ""
},
"colors": {
"transparent": "Transparente",
@@ -576,8 +557,8 @@
},
"welcomeScreen": {
"app": {
"center_heading": "Seus desenhos estão salvos no armazenamento do seu navegador.",
"center_heading_line2": "O armazenamento do navegador pode ser apagado inesperadamente.",
"center_heading": "",
"center_heading_line2": "",
"center_heading_line3": "",
"center_heading_plus": "Você queria ir para o Excalidraw+ em vez disso?",
"menuHint": "Exportar, preferências, idiomas..."
@@ -590,7 +571,7 @@
}
},
"colorPicker": {
"color": "Cor",
"color": "",
"mostUsedCustomColors": "Cores personalizadas mais usadas",
"colors": "Cores",
"shades": "Tons",
@@ -631,19 +612,18 @@
"mermaid": {
"title": "Mermaid para Excalidraw",
"button": "Inserir",
"description": "",
"description": "Atualmente apenas os diagramas<flowchartLink>Flowchart</flowchartLink><sequenceLink>Sequência,</sequenceLink> e <classLink>Class</classLink>são suportados. Os outros tipos serão renderizados como uma imagem no Excalidraw.",
"syntax": "Sintaxe em Mermaid",
"preview": "Visualizar",
"label": "Mermaid",
"inputPlaceholder": "Escreva definição de diagrama Mermaid aqui...",
"autoFixAvailable": ""
"label": "",
"inputPlaceholder": ""
},
"ttd": {
"error": "Erro!"
"error": ""
},
"chat": {
"inputPlaceholder": "",
"inputPlaceholderWithMessages": "Continuar refinando seu diagrama...",
"inputPlaceholderWithMessages": "",
"generating": "",
"rateLimitRemaining": "",
"role": {
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Colar",
"pasteAsPlaintext": "Colar como texto simples",
"pasteCharts": "Colar gráficos",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Selecionar tudo",
"multiSelect": "Adicionar elemento à seleção",
"moveCanvas": "Mover área de desenho",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Pé de coroa (muitos)",
"arrowhead_crowfoot_one": "Pé de coroa (um)",
"arrowhead_crowfoot_one_or_many": "Pé de coroa (um ou muitos)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Mais opções",
"cardinality": "",
"arrowtypes": "Tipo de seta",
"arrowtype_sharp": "Seta retilínea",
"arrowtype_round": "Seta curva",
@@ -182,11 +171,7 @@
"linkToElement": "Hiperligação para o objeto",
"wrapSelectionInFrame": "Ajustar seleção no quadro",
"tab": "",
"shapeSwitch": "",
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
"shapeSwitch": ""
},
"elementLink": {
"title": "Hiperligação para o objeto",
@@ -410,10 +395,6 @@
"errorDialog": {
"title": "Erro"
},
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": {
"disk_title": "Guardar no disco",
"disk_details": "Exportar os dados da cena para um ficheiro do qual poderá importar mais tarde.",
@@ -631,12 +612,11 @@
"mermaid": {
"title": "Mermaid para Excalidraw",
"button": "Inserir",
"description": "",
"description": "Atualmente apenas são suportados diagramas <flowchartLink>fluxo</flowchartLink>, <sequenceLink>sequência, </sequenceLink> e <classLink>classe</classLink>. Os outros tipos serão renderizados como imagem no Excalidraw.",
"syntax": "Sintaxe Mermaid",
"preview": "Pré-visualizar",
"label": "",
"inputPlaceholder": "",
"autoFixAvailable": ""
"inputPlaceholder": ""
},
"ttd": {
"error": ""

Some files were not shown because too many files have changed in this diff Show More