From d0f441dafad47675f77d16a593c5fa0ce9a599d7 Mon Sep 17 00:00:00 2001 From: Dariusz Koryto Date: Sun, 18 Oct 2026 05:26:50 +0200 Subject: [PATCH] fix(web): localize untitled notes and diagrams on public share pages (#605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(web): show the localized label for untitled notes everywhere Untitled notes are stored with the zh-CN default title, and several views only replaced an empty title, so they showed 无标题笔记 in every locale. The visible cases were the public share page (heading and browser tab), the public notebook site, AI companion cards and mentions, the note link picker, link text inserted for a note, and the title of a duplicated note. Add getMemoDisplayTitle() next to DEFAULT_MEMO_TITLE and use it in those views, matching what MemoCard already does with isDefaultMemoTitle. * fix(web): localize diagrams and content label on the public share page The public share page passed zh-CN to read-only diagrams for every language other than English, so Japanese and Polish visitors saw Chinese diagram labels. Resolve the locale with resolveSupportedLocale(), which the diagram renderer already supports for all four languages. Also translate the note content aria-label, which was hard-coded in English. --------- Co-authored-by: tianma-if --- apps/web/src/components/CompanionActionCard.tsx | 5 +++-- apps/web/src/components/CompanionChat.tsx | 4 ++-- apps/web/src/components/CompanionRunTimeline.tsx | 3 ++- .../src/components/CompanionToolActionCard.tsx | 5 +++-- apps/web/src/components/EditorPane.tsx | 3 ++- apps/web/src/components/PublicNotebookSite.tsx | 7 ++++--- apps/web/src/components/PublicSharePage.tsx | 16 ++++++---------- apps/web/src/components/WorkspaceApp.tsx | 4 ++-- .../components/editor/EditorNoteLinkPicker.tsx | 3 ++- packages/shared/src/content.test.mjs | 15 +++++++++++++++ packages/shared/src/content.ts | 9 +++++++++ packages/shared/src/i18n/en-US.ts | 1 + packages/shared/src/i18n/ja.ts | 1 + packages/shared/src/i18n/pl.ts | 1 + packages/shared/src/i18n/zh-CN.ts | 1 + 15 files changed, 54 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/CompanionActionCard.tsx b/apps/web/src/components/CompanionActionCard.tsx index de1c9c85..43e89d57 100644 --- a/apps/web/src/components/CompanionActionCard.tsx +++ b/apps/web/src/components/CompanionActionCard.tsx @@ -1,4 +1,5 @@ import type { CompanionAction } from "@edgeever/shared"; +import { getMemoDisplayTitle } from "@edgeever/shared"; import { ArrowUpRight, Check, FileText, GitMerge } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; @@ -37,7 +38,7 @@ export function CompanionActionCard({ action, busy, onApply, onDismiss, onOpenNo onClick={() => onOpenNote(note.id, note.notebookId)} > - {note.title || t("common.untitledMemo")} + {getMemoDisplayTitle(note.title, t("common.untitledMemo"))} {note.excerpt ? ( @@ -63,7 +64,7 @@ export function CompanionActionCard({ action, busy, onApply, onDismiss, onOpenNo className="group/note inline-flex max-w-full items-center gap-1.5 text-xs font-semibold text-slate-800 hover:text-slate-950 " > - {action.notes[0].title || t("common.untitledMemo")} + {getMemoDisplayTitle(action.notes[0].title, t("common.untitledMemo"))} {action.notes[0].excerpt ? ( diff --git a/apps/web/src/components/CompanionChat.tsx b/apps/web/src/components/CompanionChat.tsx index 251e3c96..0efd1ceb 100644 --- a/apps/web/src/components/CompanionChat.tsx +++ b/apps/web/src/components/CompanionChat.tsx @@ -4,7 +4,7 @@ import { FileText, Folder, Loader2, MessageCircle, Tag, X } from "lucide-react"; import type { CompanionAction, CompanionAnswer, CompanionDiscoverySettings, CompanionEvent, CompanionMention, CompanionTurn, CompanionTurnInput, } from "@edgeever/shared"; -import { parseCompanionMentionQuery } from "@edgeever/shared"; +import { parseCompanionMentionQuery, getMemoDisplayTitle } from "@edgeever/shared"; import { Button } from "@/components/ui/button"; import { api, ApiRequestError } from "@/lib/api"; import { companionLocale } from "@/lib/companion-locale"; @@ -118,7 +118,7 @@ export function CompanionChat({ if (cancelled || !alive.current) return; const lower = query.toLowerCase(); setMentionHits([ - ...memos.memos.slice(0, 5).map(memo => ({ type: "memo" as const, id: memo.id, title: memo.title || t("common.untitledMemo") })), + ...memos.memos.slice(0, 5).map(memo => ({ type: "memo" as const, id: memo.id, title: getMemoDisplayTitle(memo.title, t("common.untitledMemo")) })), ...notebooks.notebooks.filter(notebook => !query || notebook.name.toLowerCase().includes(lower)).slice(0, 4) .map(notebook => ({ type: "notebook" as const, id: notebook.id, title: notebook.name })), ...tags.tags.filter(tag => !query || tag.name.toLowerCase().includes(lower)).slice(0, 4) diff --git a/apps/web/src/components/CompanionRunTimeline.tsx b/apps/web/src/components/CompanionRunTimeline.tsx index d6811f8d..fc48de35 100644 --- a/apps/web/src/components/CompanionRunTimeline.tsx +++ b/apps/web/src/components/CompanionRunTimeline.tsx @@ -1,4 +1,5 @@ import type { CompanionTodo, CompanionToolCall, CompanionToolEffect } from "@edgeever/shared"; +import { getMemoDisplayTitle } from "@edgeever/shared"; import { Check, ChevronDown, Circle, FileText, Loader2, RotateCcw, Undo2 } from "lucide-react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -61,7 +62,7 @@ function CompanionToolList({ {effect.kind === "trashed" ? ( {note.excerpt ? ( diff --git a/apps/web/src/components/EditorPane.tsx b/apps/web/src/components/EditorPane.tsx index 523f01ac..e269688d 100644 --- a/apps/web/src/components/EditorPane.tsx +++ b/apps/web/src/components/EditorPane.tsx @@ -153,6 +153,7 @@ import { getAiDocumentFingerprint, isAiSelectionSnapshotCurrent, parseMemoLinkHref, + getMemoDisplayTitle, } from "@edgeever/shared"; import { NEW_IMAGE_WIDTH_PERCENT } from "@edgeever/shared/image-display"; import { EdgeEverLink } from "@edgeever/shared/editor-link"; @@ -1517,7 +1518,7 @@ const RichEditorPane = ({ .focus() .insertContent({ type: "text", - text: selectedText || target.title || t("common.untitledMemo"), + text: selectedText || getMemoDisplayTitle(target.title, t("common.untitledMemo")), marks: [{ type: "link", attrs: { href: createMemoLinkHref(target.id), class: "edgeever-note-link" } }], }) .run(); diff --git a/apps/web/src/components/PublicNotebookSite.tsx b/apps/web/src/components/PublicNotebookSite.tsx index 3959e9a3..eaa8c9db 100644 --- a/apps/web/src/components/PublicNotebookSite.tsx +++ b/apps/web/src/components/PublicNotebookSite.tsx @@ -1,4 +1,5 @@ import type { PublicNotebookShare } from "@edgeever/shared"; +import { getMemoDisplayTitle } from "@edgeever/shared"; import { ArrowLeft, ArrowRight, @@ -63,7 +64,7 @@ const NavigationNotes = ({ return (
    {memos.map((memo) => { - const title = memo.title?.trim() || t("common.untitledMemo"); + const title = getMemoDisplayTitle(memo.title, t("common.untitledMemo")); return (
  • @@ -144,7 +145,7 @@ const NoteTitle = ({ memo }: { memo: Memo }) => { const { t } = useTranslation(); return ( - {memo.title?.trim() || t("common.untitledMemo")} + {getMemoDisplayTitle(memo.title, t("common.untitledMemo"))} ); }; @@ -307,7 +308,7 @@ export const PublicNotebookSite = ({ const matches = useMemo( () => notes.filter(({ memo }) => - (memo.title?.trim() || t("common.untitledMemo")) + getMemoDisplayTitle(memo.title, t("common.untitledMemo")) .toLocaleLowerCase() .includes(query), ), diff --git a/apps/web/src/components/PublicSharePage.tsx b/apps/web/src/components/PublicSharePage.tsx index b265a050..a45c2c06 100644 --- a/apps/web/src/components/PublicSharePage.tsx +++ b/apps/web/src/components/PublicSharePage.tsx @@ -24,7 +24,9 @@ import { resolveNoteProse, type PublicMemoShare, type ShareBodyAccess, + getMemoDisplayTitle, } from "@edgeever/shared"; +import { resolveSupportedLocale } from "@edgeever/shared/i18n/locales"; import { applyEditorBodyFontPreference } from "@/lib/editor-body-font"; import { createEdgeEverMathematics } from "@edgeever/shared/mathematics"; import { PdfAttachment } from "@/components/editor/PdfAttachment"; @@ -104,7 +106,7 @@ const SharedRichText = ({ content }: { content: PublicMemoShare["contentJson"] } editorProps: { attributes: { class: "prose prose-slate max-w-none focus:outline-none", - "aria-label": "Shared note content", + "aria-label": t("sharing.contentLabel"), }, }, }, [content]); @@ -239,7 +241,7 @@ export const PublicSharePage = () => { document.head.appendChild(robots); if (share) { document.title = withEnvironmentTitlePrefix( - `${share.title?.trim() || t("common.untitledMemo")} · EdgeEver`, + `${getMemoDisplayTitle(share.title, t("common.untitledMemo"))} · EdgeEver`, { development: import.meta.env.DEV, profile: __EDGEEVER_DEVELOPMENT_PROFILE__ }, ); } else if (passwordRequired) { @@ -364,7 +366,7 @@ export const PublicShareArticle = ({ layout === "site" && "order-1", )} > - {share.title?.trim() || t("common.untitledMemo")} + {getMemoDisplayTitle(share.title, t("common.untitledMemo"))} {share.tags.length > 0 ? (
    ) : null} ( onInsert(candidate)}> - {candidate.title || t("common.untitledMemo")} + {getMemoDisplayTitle(candidate.title, t("common.untitledMemo"))} {candidate.excerpt} ))} diff --git a/packages/shared/src/content.test.mjs b/packages/shared/src/content.test.mjs index 89f0f000..53314dae 100644 --- a/packages/shared/src/content.test.mjs +++ b/packages/shared/src/content.test.mjs @@ -1,6 +1,8 @@ import { describe, expect, test } from "bun:test"; import { countMemoCharacters, + DEFAULT_MEMO_TITLE, + getMemoDisplayTitle, docToMarkdown, docToText, markdownToDoc, @@ -812,3 +814,16 @@ describe("plugin embed Markdown compatibility", () => { expect(resolveMemoContentDoc(parsed, "fallback").content[0].type).toBe(PLUGIN_EMBED_NODE_TYPE); }); }); + +describe("getMemoDisplayTitle", () => { + test("shows the localized label for empty and default untitled titles", () => { + expect(getMemoDisplayTitle("", "Untitled note")).toBe("Untitled note"); + expect(getMemoDisplayTitle(null, "Untitled note")).toBe("Untitled note"); + expect(getMemoDisplayTitle(` ${DEFAULT_MEMO_TITLE} `, "Notatka bez tytułu")).toBe("Notatka bez tytułu"); + }); + + test("keeps real titles, including Chinese ones", () => { + expect(getMemoDisplayTitle(" Plan ", "Untitled note")).toBe("Plan"); + expect(getMemoDisplayTitle("会议记录", "Untitled note")).toBe("会议记录"); + }); +}); diff --git a/packages/shared/src/content.ts b/packages/shared/src/content.ts index d3e61fa7..60c94aa0 100644 --- a/packages/shared/src/content.ts +++ b/packages/shared/src/content.ts @@ -78,6 +78,15 @@ export type TiptapDoc = { export const DEFAULT_MEMO_TITLE = "无标题笔记"; +/** + * Title to show for a note. Untitled notes are stored with the zh-CN default + * title, so it is replaced by the reader's localized label like an empty one. + */ +export const getMemoDisplayTitle = (title: string | null | undefined, untitledLabel: string) => { + const trimmed = title?.trim() ?? ""; + return trimmed && trimmed !== DEFAULT_MEMO_TITLE ? trimmed : untitledLabel; +}; + export const resolveMergedMemoTitle = ( inputTitle: string | null | undefined, sourceMemos: Array<{ title: string | null | undefined }>, diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index 66538136..c560f731 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -2618,6 +2618,7 @@ export const enUS = { expandOutlineHeading: "Expand {{name}}", }, sharing: { + contentLabel: "Shared note content", siteHideContents: "Hide contents", siteShowContents: "Show contents", siteHome: "Home", diff --git a/packages/shared/src/i18n/ja.ts b/packages/shared/src/i18n/ja.ts index e9007571..53428ebd 100644 --- a/packages/shared/src/i18n/ja.ts +++ b/packages/shared/src/i18n/ja.ts @@ -2607,6 +2607,7 @@ export const ja = { expandOutlineHeading: "{{name}} を開く", }, sharing: { + contentLabel: "共有されたノートの内容", siteHideContents: "目次を隠す", siteShowContents: "目次を表示", siteHome: "ホーム", diff --git a/packages/shared/src/i18n/pl.ts b/packages/shared/src/i18n/pl.ts index 466081a8..5ad5184d 100644 --- a/packages/shared/src/i18n/pl.ts +++ b/packages/shared/src/i18n/pl.ts @@ -2607,6 +2607,7 @@ export const pl = { expandOutlineHeading: "Rozwiń {{name}}", }, sharing: { + contentLabel: "Treść udostępnionej notatki", siteHideContents: "Ukryj spis treści", siteShowContents: "Pokaż spis treści", siteHome: "Strona główna", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 547568d1..6f68dc66 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -2616,6 +2616,7 @@ export const zhCN = { expandOutlineHeading: "展开 {{name}}", }, sharing: { + contentLabel: "分享的笔记内容", siteHideContents: "收起目录", siteShowContents: "展开目录", siteHome: "首页",