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