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: "首页",