fix(web): localize untitled notes and diagrams on public share pages (#605)
* 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 <yingwaizhiying@gmail.com>
This commit is contained in:
1 parent
82dac9b5cb
commit
d0f441dafa
15 files changed
+54
-24
No files matched your search
@@ -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)}
|
||||
>
|
||||
<FileText className="h-3 w-3 shrink-0 text-slate-400" />
|
||||
<span className="truncate">{note.title || t("common.untitledMemo")}</span>
|
||||
<span className="truncate">{getMemoDisplayTitle(note.title, t("common.untitledMemo"))}</span>
|
||||
<ArrowUpRight className="h-2.5 w-2.5 shrink-0 opacity-50" />
|
||||
</button>
|
||||
{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 "
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5 shrink-0 text-slate-400 group-hover/note:text-slate-950 " />
|
||||
<span className="truncate">{action.notes[0].title || t("common.untitledMemo")}</span>
|
||||
<span className="truncate">{getMemoDisplayTitle(action.notes[0].title, t("common.untitledMemo"))}</span>
|
||||
<ArrowUpRight className="h-3 w-3 shrink-0 opacity-40 group-hover/note:opacity-100" />
|
||||
</button>
|
||||
{action.notes[0].excerpt ? (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
<button type="button" disabled={busy} className="inline-flex max-w-full items-center gap-1 font-medium hover:text-slate-950 hover:underline"
|
||||
onClick={() => onOpenNote(effect.memoId!, effect.notebookId || "")}>
|
||||
<FileText className="h-3 w-3 shrink-0 text-slate-400" />
|
||||
<span className="truncate">{effect.title || t("common.untitledMemo")}</span>
|
||||
<span className="truncate">{getMemoDisplayTitle(effect.title, t("common.untitledMemo"))}</span>
|
||||
</button>
|
||||
{effect.kind === "trashed" ? (
|
||||
<Button type="button" size="sm" variant="ghost" className="h-6 px-1.5 text-xs" disabled={busy || pending === key}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CompanionAction } from "@edgeever/shared";
|
||||
import { getMemoDisplayTitle } from "@edgeever/shared";
|
||||
import { ArrowUpRight, Check, FileText } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -12,7 +13,7 @@ export function CompanionToolActionCard({ action, busy, onApply, onDismiss, onOp
|
||||
if (action.plan.kind !== "tool") return null;
|
||||
const name = action.plan.toolName;
|
||||
const title = t(`companion.actions.tools.${name}`, { defaultValue: name });
|
||||
const names = new Map([...action.notes.map(note => [note.id, note.title || t("common.untitledMemo")] as const),
|
||||
const names = new Map([...action.notes.map(note => [note.id, getMemoDisplayTitle(note.title, t("common.untitledMemo"))] as const),
|
||||
...(action.preview?.notebooks ?? []).map(notebook => [notebook.id, notebook.name] as const)]);
|
||||
const display = (value: unknown): string => {
|
||||
if (typeof value === "string") return names.has(value) ? `${names.get(value)} (${value})` : value;
|
||||
@@ -45,7 +46,7 @@ export function CompanionToolActionCard({ action, busy, onApply, onDismiss, onOp
|
||||
className="inline-flex max-w-full items-center gap-1 text-xs font-medium text-slate-800 hover:text-slate-950 hover:underline "
|
||||
>
|
||||
<FileText className="h-3 w-3 shrink-0 text-slate-400" />
|
||||
<span className="truncate">{note.title || t("common.untitledMemo")}</span>
|
||||
<span className="truncate">{getMemoDisplayTitle(note.title, t("common.untitledMemo"))}</span>
|
||||
<ArrowUpRight className="h-2.5 w-2.5 shrink-0 opacity-50" />
|
||||
</button>
|
||||
{note.excerpt ? (
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 (
|
||||
<ul className="space-y-0.5">
|
||||
{memos.map((memo) => {
|
||||
const title = memo.title?.trim() || t("common.untitledMemo");
|
||||
const title = getMemoDisplayTitle(memo.title, t("common.untitledMemo"));
|
||||
return (
|
||||
<li key={memo.id}>
|
||||
<Tooltip>
|
||||
@@ -144,7 +145,7 @@ const NoteTitle = ({ memo }: { memo: Memo }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<span className="min-w-0 break-words">
|
||||
{memo.title?.trim() || t("common.untitledMemo")}
|
||||
{getMemoDisplayTitle(memo.title, t("common.untitledMemo"))}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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"))}
|
||||
</h1>
|
||||
{share.tags.length > 0 ? (
|
||||
<div
|
||||
@@ -401,13 +403,7 @@ export const PublicShareArticle = ({
|
||||
/>
|
||||
) : null}
|
||||
<SharedDocument
|
||||
locale={
|
||||
(i18n.resolvedLanguage || i18n.language || "zh-CN").startsWith(
|
||||
"en",
|
||||
)
|
||||
? "en-US"
|
||||
: "zh-CN"
|
||||
}
|
||||
locale={resolveSupportedLocale(i18n.resolvedLanguage || i18n.language)}
|
||||
share={share}
|
||||
token={token}
|
||||
access={access}
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
} from "@/lib/mobile-editor";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isBrowserOffline, isBrowserOnline } from "@/lib/network-status";
|
||||
import { createDefaultDiagramDocument, createDefaultInfographicDocument, hasPosterDocumentMarker, posterFallbackMarkdown, serializePosterDocument, createDefaultTableDocument, diagramFallbackMarkdown, getNotebookScopeIds, hasTableDocumentMarker, infographicFallbackMarkdown, markdownToDoc, parseDiagramDocument, parseInfographicDocument, parseTableDocument, serializeDiagramDocument, serializeInfographicDocument, serializeTableDocument, tableFallbackMarkdown, type NoteCreateKind, type Notebook, type AuthUser, type MemoSummary, type MemoDetail, type MemoTemplate as SavedMemoTemplate } from "@edgeever/shared";
|
||||
import { createDefaultDiagramDocument, createDefaultInfographicDocument, hasPosterDocumentMarker, posterFallbackMarkdown, serializePosterDocument, createDefaultTableDocument, diagramFallbackMarkdown, getNotebookScopeIds, hasTableDocumentMarker, infographicFallbackMarkdown, markdownToDoc, parseDiagramDocument, parseInfographicDocument, parseTableDocument, serializeDiagramDocument, serializeInfographicDocument, serializeTableDocument, tableFallbackMarkdown, type NoteCreateKind, type Notebook, type AuthUser, type MemoSummary, type MemoDetail, type MemoTemplate as SavedMemoTemplate, getMemoDisplayTitle } from "@edgeever/shared";
|
||||
import { toggleMobileMemoSelection } from "@edgeever/shared/mobile-ui";
|
||||
import type {
|
||||
Pane,
|
||||
@@ -2151,7 +2151,7 @@ export const WorkspaceApp = ({
|
||||
if (duplicateMemoMutation.isPending || memoView === "trash") return;
|
||||
duplicateMemoMutation.mutate({
|
||||
memoId: memo.id,
|
||||
title: t("editor.duplicateTitle", { title: memo.title?.trim() || t("common.untitledMemo") }).slice(0, 160),
|
||||
title: t("editor.duplicateTitle", { title: getMemoDisplayTitle(memo.title, t("common.untitledMemo")) }).slice(0, 160),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link2, X } from "lucide-react";
|
||||
import { getMemoDisplayTitle } from "@edgeever/shared";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { MemoSummary } from "@edgeever/shared";
|
||||
import {
|
||||
@@ -66,7 +67,7 @@ export const EditorNoteLinkPicker = ({
|
||||
{candidates.map((candidate) => (
|
||||
<CommandItem key={candidate.id} value={candidate.id} onSelect={() => onInsert(candidate)}>
|
||||
<Link2 className="h-4 w-4 shrink-0 text-slate-600" />
|
||||
<span className="min-w-0 flex-1 truncate">{candidate.title || t("common.untitledMemo")}</span>
|
||||
<span className="min-w-0 flex-1 truncate">{getMemoDisplayTitle(candidate.title, t("common.untitledMemo"))}</span>
|
||||
<span className="max-w-40 truncate text-xs text-slate-400">{candidate.excerpt}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
|
||||
@@ -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("会议记录");
|
||||
});
|
||||
});
|
||||
@@ -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 }>,
|
||||
|
||||
@@ -2618,6 +2618,7 @@ export const enUS = {
|
||||
expandOutlineHeading: "Expand {{name}}",
|
||||
},
|
||||
sharing: {
|
||||
contentLabel: "Shared note content",
|
||||
siteHideContents: "Hide contents",
|
||||
siteShowContents: "Show contents",
|
||||
siteHome: "Home",
|
||||
|
||||
@@ -2607,6 +2607,7 @@ export const ja = {
|
||||
expandOutlineHeading: "{{name}} を開く",
|
||||
},
|
||||
sharing: {
|
||||
contentLabel: "共有されたノートの内容",
|
||||
siteHideContents: "目次を隠す",
|
||||
siteShowContents: "目次を表示",
|
||||
siteHome: "ホーム",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2616,6 +2616,7 @@ export const zhCN = {
|
||||
expandOutlineHeading: "展开 {{name}}",
|
||||
},
|
||||
sharing: {
|
||||
contentLabel: "分享的笔记内容",
|
||||
siteHideContents: "收起目录",
|
||||
siteShowContents: "展开目录",
|
||||
siteHome: "首页",
|
||||
|
||||
Reference in new issue
Block a user