Put the interface into English, and give the notebook one way in

The interface had drifted into two languages — the frame in English, everything
added later in German — which is worse than either, since the same idea appeared
under two names depending on when that part was written. Sorting by title still
uses the German collation, so Übung 2 comes before Übung 10: that is about the
notes, not the chrome.

Two archive folders were renamed with it (bilder/ → images/, zeichnungen/ →
drawings/). The importer reads both spellings, and both the old and new marker
line for a drawing — archives exported before this are on disk, and an import
that quietly dropped their drawings would be the worst kind of tidy-up.

Making a note, uploading a PDF and importing Markdown were three buttons side by
side, two of them icons you had to hover to identify. They are the same act —
putting something new in this notebook — so they are now one + menu.

The search box moves out of the sidebar and into the note list, labelled with the
notebook it narrows. Nothing changed about what it does: it has always searched
only the notebook you are in, but from the sidebar it read as searching
everything.

A separator no longer carries a sentence explaining the order the sort menu
already names. Renaming joins removing on the right-click menu — a left-click on
the label started an edit, so the row could not be touched without falling into
one.

218 unit tests, 238 browser checks across eleven suites. Two of those caught real
breakage from this change rather than needing a translation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-11 02:17:22 +02:00
1 parent 0a7cf26b28
commit 52bd2b8f3d
27 files changed
+469 -336

No files matched your search

+16 -16
View File
@@ -75,7 +75,7 @@ async function loadNote(meta: NoteMeta): Promise<Loaded> {
/** Unique filename per note — two notes may well share a title. */
function nameFor(meta: NoteMeta, taken: Set<string>): string {
const base = safeFilename(meta.title, "Notiz");
const base = safeFilename(meta.title, "Note");
let name = base;
for (let i = 2; taken.has(name.toLowerCase()); i++) name = `${base} (${i})`;
taken.add(name.toLowerCase());
@@ -92,9 +92,9 @@ function nameFor(meta: NoteMeta, taken: Set<string>): string {
* so a clean copy stays available however often it has been drawn on.
*/
export async function exportPdfNote(meta: NoteMeta, withInk: boolean): Promise<void> {
if (!meta.pdfAttachmentId) throw new Error("Zu dieser Notiz gehört keine PDF-Datei.");
if (!meta.pdfAttachmentId) throw new Error("This note has no PDF attached.");
const bytes = await sync.loadPdfBytes(meta.pdfAttachmentId);
const name = safeFilename(meta.title, "Dokument");
const name = safeFilename(meta.title, "Document");
if (!withInk) {
download(bytesToBlob(bytes, "application/pdf"), `${name}.pdf`);
@@ -111,7 +111,7 @@ export async function exportPdfNote(meta: NoteMeta, withInk: boolean): Promise<v
}
const stamped = await stampStrokesIntoPdf(bytes, ink);
download(bytesToBlob(stamped, "application/pdf"), `${name} (mit Notizen).pdf`);
download(bytesToBlob(stamped, "application/pdf"), `${name} (annotated).pdf`);
}
/** True when the PDF carries drawings or pinned notes, so the wizard can say so. */
@@ -143,7 +143,7 @@ export async function noteHasDrawing(meta: NoteMeta): Promise<boolean> {
export async function exportDrawingAsPng(meta: NoteMeta): Promise<void> {
const { content } = await loadNote(meta);
const png = await drawingToPng(content.drawingStrokes);
if (!png) throw new Error("Diese Notiz enthält keine Zeichnung.");
if (!png) throw new Error("This note has no drawing.");
download(bytesToBlob(png, "image/png"), `${safeFilename(meta.title)}.png`);
}
@@ -157,7 +157,7 @@ export async function exportNoteAsMarkdown(meta: NoteMeta): Promise<void> {
title: meta.title,
updatedAt: meta.updatedAt,
hasDrawing,
attachmentPath: (id) => `${safeFilename(meta.title)}-bilder/${id}.png`,
attachmentPath: (id) => `${safeFilename(meta.title)}-images/${id}.png`,
notePath: () => null, // a lone note has nothing to link to
});
@@ -171,10 +171,10 @@ export async function exportNoteAsMarkdown(meta: NoteMeta): Promise<void> {
const entries: ZipEntry[] = [{ name: `${folder}.md`, data: md }];
for (const id of ids) {
const bytes = await fetchAttachment(id);
if (bytes) entries.push({ name: `${folder}-bilder/${id}.png`, data: bytes });
if (bytes) entries.push({ name: `${folder}-images/${id}.png`, data: bytes });
}
const png = await drawingToPng(content.drawingStrokes);
if (png) entries.push({ name: `${folder}-zeichnung.png`, data: png });
if (png) entries.push({ name: `${folder}-drawing.png`, data: png });
download(createZip(entries), `${folder}.zip`);
}
@@ -229,7 +229,7 @@ export async function exportNotesAsPdf(
}
}
onProgress?.({ done: total, total, label: "Druckansicht wird geöffnet" });
onProgress?.({ done: total, total, label: "Opening the print view" });
await printNotes(printable, title);
}
@@ -275,7 +275,7 @@ export async function exportNotesAsZip({
const index: string[] = [
`# ${label}`,
"",
`${notes.length} Notizen, exportiert am ${new Date().toLocaleString("de-DE")}`,
`${notes.length} notes, exported ${new Date().toLocaleString()}`,
"",
];
const byNotebook = new Map<string | null, NoteMeta[]>();
@@ -300,24 +300,24 @@ export async function exportNotesAsZip({
title: meta.title,
updatedAt: meta.updatedAt,
hasDrawing,
attachmentPath: (id) => `bilder/${id}.png`,
attachmentPath: (id) => `images/${id}.png`,
notePath,
}),
});
for (const id of attachmentIds(doc)) {
if (entries.some((e) => e.name === `bilder/${id}.png`)) continue;
if (entries.some((e) => e.name === `images/${id}.png`)) continue;
const bytes = await fetchAttachment(id);
if (bytes) entries.push({ name: `bilder/${id}.png`, data: bytes });
if (bytes) entries.push({ name: `images/${id}.png`, data: bytes });
}
const png = await drawingToPng(content.drawingStrokes);
if (png) entries.push({ name: `zeichnungen/${name}.png`, data: png });
if (png) entries.push({ name: `drawings/${name}.png`, data: png });
} catch (err) {
// A note that cannot be read is named in the index rather than skipped in
// silence — an export that quietly drops things is worse than useless.
entries.push({
name: `${name}.md`,
data: `# ${meta.title}\n\n> Diese Notiz konnte beim Export nicht gelesen werden.\n> (${(err as Error).message})\n`,
data: `# ${meta.title}\n\n> This note could not be read when exporting.\n> (${(err as Error).message})\n`,
});
}
done++;
@@ -325,7 +325,7 @@ export async function exportNotesAsZip({
for (const [notebookId, group] of byNotebook) {
const nb = notebooks.find((b) => b.id === notebookId);
index.push("", `## ${nb?.name || "Ohne Notizbuch"}`, "");
index.push("", `## ${nb?.name || "No notebook"}`, "");
for (const n of group) index.push(`- [${n.title || "Untitled"}](${notePath(n.id)})`);
}
entries.unshift({ name: "README.md", data: index.join("\n") + "\n" });
+2 -2
View File
@@ -183,7 +183,7 @@ describe("document framing", () => {
it("says when a drawing could not come along, instead of dropping it silently", () => {
const out = md(doc(para(text("Text"))), { hasDrawing: true });
expect(out).toContain("Zeichnung");
expect(out).toContain("drawing");
});
it("collapses the blank lines the block rules emit generously", () => {
@@ -208,7 +208,7 @@ describe("safeFilename", () => {
});
it("falls back when nothing usable is left", () => {
expect(safeFilename("")).toBe("Notiz");
expect(safeFilename("")).toBe("Note");
expect(safeFilename("///")).toBe("---");
});
+4 -4
View File
@@ -208,7 +208,7 @@ export function noteToMarkdown(doc: DocNode, options: MarkdownOptions = {}): str
if (options.title) {
lines.push(`# ${options.title}`, "");
if (options.updatedAt) {
lines.push(`*${new Date(options.updatedAt).toLocaleString("de-DE")}*`, "");
lines.push(`*${new Date(options.updatedAt).toLocaleString()}*`, "");
}
}
@@ -227,8 +227,8 @@ export function noteToMarkdown(doc: DocNode, options: MarkdownOptions = {}): str
lines.push(...blocks);
if (options.hasDrawing) {
lines.push("", "> ✏️ Diese Notiz enthält eine Zeichnung, die Markdown nicht abbilden kann.");
lines.push("> Sie liegt als PNG neben der Datei.");
lines.push("", "> ✏️ This note has a drawing, which Markdown cannot hold.");
lines.push("> It sits beside this file as a PNG.");
}
// Collapse runs of blank lines; the block rules emit them generously.
@@ -239,7 +239,7 @@ export function noteToMarkdown(doc: DocNode, options: MarkdownOptions = {}): str
}
/** Filename-safe version of a note title. */
export function safeFilename(title: string, fallback = "Notiz"): string {
export function safeFilename(title: string, fallback = "Note"): string {
const cleaned = (title || "")
.replace(/[/\\?%*:|"<>]/g, "-")
.replace(/\s+/g, " ")