From e89226630e773a5ed7c065037d5ae77061a8b8a2 Mon Sep 17 00:00:00 2001 From: Richard <1464315342s@gmail.com> Date: Sat, 25 Jul 2026 02:13:24 +1000 Subject: [PATCH 1/2] fix(i18n): localize article editor page --- app/[locale]/editor/EditorPageClient.tsx | 66 +++++++++++++----------- app/components/MarkdownEditor.tsx | 20 +++++-- messages/en.json | 35 ++++++++++++- messages/zh.json | 35 ++++++++++++- 4 files changed, 116 insertions(+), 40 deletions(-) diff --git a/app/[locale]/editor/EditorPageClient.tsx b/app/[locale]/editor/EditorPageClient.tsx index 3dcb2912..55e179b0 100644 --- a/app/[locale]/editor/EditorPageClient.tsx +++ b/app/[locale]/editor/EditorPageClient.tsx @@ -10,6 +10,7 @@ import { Button } from "@/app/components/ui/button"; import { useCallback, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; +import { useTranslations } from "next-intl"; import type { UserView } from "@/lib/use-auth"; import type { PostRequest, ApiResponse, PostView } from "@/app/types/post"; import { @@ -38,6 +39,7 @@ function titleToSlug(title: string): string { export function EditorPageClient({ user }: EditorPageClientProps) { const router = useRouter(); + const t = useTranslations("editor"); const [isPublishing, setIsPublishing] = useState(false); const [imageCount, setImageCount] = useState(0); const editorRef = useRef(null); @@ -61,9 +63,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) { // 否则 R2 返 403 SignatureDoesNotMatch。 const primaryMime = file.type.split(";")[0]!.trim().toLowerCase(); if (!primaryMime) { - throw new Error( - `无法识别图片类型:${file.name}(浏览器未给出 MIME),请另存为 PNG/JPG/WebP 后重试`, - ); + throw new Error(t("errors.imageType", { filename: file.name })); } const token = localStorage.getItem("satoken") ?? ""; @@ -83,7 +83,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) { if (!response.ok) { const error = await response.json(); - throw new Error(error.error || "获取上传链接失败"); + throw new Error(error.error || t("errors.uploadLink")); } const { uploadUrl, publicUrl } = await response.json(); @@ -96,7 +96,9 @@ export function EditorPageClient({ user }: EditorPageClientProps) { }); if (!uploadResponse.ok) { - throw new Error(`上传图片失败: ${uploadResponse.statusText}`); + throw new Error( + t("errors.imageUpload", { statusText: uploadResponse.statusText }), + ); } return { blobUrl, publicUrl }; @@ -107,7 +109,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) { try { if (!title.trim()) { - alert("请输入文章标题"); + alert(t("errors.titleRequired")); return; } @@ -117,9 +119,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) { : titleToSlug(title); if (rawSlug && !FILENAME_PATTERN.test(rawSlug)) { - alert( - "文件名仅支持字母、数字、连字符或下划线,并需以字母或数字开头(已自动清洗空格和特殊符号)。", - ); + alert(t("errors.invalidFilename")); return; } @@ -133,7 +133,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) { const editorHandle = editorRef.current; if (!editorHandle) { - throw new Error("编辑器尚未就绪,无法上传图片"); + throw new Error(t("errors.editorNotReady")); } // 清理编辑器中未被 Markdown 正文引用的孤儿图片 @@ -158,7 +158,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) { const token = localStorage.getItem("satoken") ?? ""; if (!token) { - throw new Error("请先登录后再发布"); + throw new Error(t("errors.loginRequired")); } const postRequest: PostRequest = { @@ -186,20 +186,25 @@ export function EditorPageClient({ user }: EditorPageClientProps) { const body = await res.json().catch(() => ({})); throw new Error( (body as { message?: string }).message ?? - `发布失败(HTTP ${res.status})`, + t("errors.publishFailedHttp", { status: res.status }), ); } const body = (await res.json()) as ApiResponse; if (!body.success || !body.data) { - throw new Error(body.message ?? "发布失败,请重试"); + throw new Error(body.message ?? t("errors.publishFailedRetry")); } const { slug: finalSlug, authorUsername } = body.data; router.push(`/u/${authorUsername}/posts/${finalSlug}`); } catch (error) { console.error("发布失败:", error); - alert(`发布失败:${error instanceof Error ? error.message : "未知错误"}`); + alert( + t("errors.publishFailed", { + message: + error instanceof Error ? error.message : t("errors.unknownError"), + }), + ); } finally { setIsPublishing(false); } @@ -212,13 +217,11 @@ export function EditorPageClient({ user }: EditorPageClientProps) { {/* 头部 */}
-

写篇文章

-

- 写完直接发布,想进知识库再一键投稿。 -

+

{t("pageTitle")}

+

{t("pageSubtitle")}

- +
@@ -230,14 +233,15 @@ export function EditorPageClient({ user }: EditorPageClientProps) { {/* Markdown 编辑器 */}
-

文章内容

+

{t("contentHeading")}

- {markdown.length} 字符 · {imageCount} 张图片 + {t("stats", { characters: markdown.length, images: imageCount })}
@@ -245,16 +249,16 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
{!title.trim() ? ( - 请填写标题 + {t("titleRequired")} ) : previewSlug ? ( - 将发布到{" "} + {t("publishTo")}{" "} /u/{user.username}/posts/{previewSlug} ) : ( - 发布后 slug 由标题自动生成 + {t("autoSlug")} )}
@@ -262,28 +266,28 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
{/* 流程提示 */}
-

写完直接发

+

{t("directPublish")}

    -
  • 图片粘贴后自动上传到 CDN,发布时无需额外处理
  • -
  • 发布即可见,链接可直接分享,不等 review
  • -
  • 想进知识库?发布后点「收录进知识库」一键投稿
  • +
  • {t("tips.imageUpload")}
  • +
  • {t("tips.publishImmediately")}
  • +
  • {t("tips.promoteToDocs")}
diff --git a/app/components/MarkdownEditor.tsx b/app/components/MarkdownEditor.tsx index 7e901e72..3d4b6f6c 100644 --- a/app/components/MarkdownEditor.tsx +++ b/app/components/MarkdownEditor.tsx @@ -23,6 +23,7 @@ import "@milkdown/crepe/theme/frame.css"; interface MarkdownEditorProps { onImagesChange?: (count: number) => void; + defaultMarkdown: string; } export interface MarkdownEditorHandle { @@ -42,11 +43,12 @@ export interface MarkdownEditorHandle { export const MarkdownEditor = forwardRef< MarkdownEditorHandle, MarkdownEditorProps ->(function MarkdownEditor({ onImagesChange }, ref) { +>(function MarkdownEditor({ onImagesChange, defaultMarkdown }, ref) { const editorRef = useRef(null); const crepeInstanceRef = useRef(null); const isLoadingRef = useRef(false); const markdownRef = useRef(useEditorStore.getState().markdown); + const lastDefaultMarkdownRef = useRef(null); const { markdown, setMarkdown } = useEditorStore(); const { appendFile, cleanupUnreferenced, clearAll, getSnapshot } = useImageBuffer(onImagesChange); @@ -110,11 +112,19 @@ export const MarkdownEditor = forwardRef< return nodes; }; + // 默认示例也会同步到 store。切换语言时,只有内容仍等于上次注入的 + // 默认示例才替换为新语言;用户修改过的草稿必须保留。 + const shouldUseDefault = + !markdownRef.current || + markdownRef.current === lastDefaultMarkdownRef.current; + const initialMarkdown = shouldUseDefault + ? defaultMarkdown + : markdownRef.current; + lastDefaultMarkdownRef.current = initialMarkdown; + const crepe = new Crepe({ root: editorRef.current!, - defaultValue: - markdownRef.current || - "# 开始写作...\n\n在这里输入你的 Markdown 内容。\n\n支持粘贴图片!", + defaultValue: initialMarkdown, featureConfigs: { [Crepe.Feature.ImageBlock]: imageBlockConfig, }, @@ -179,7 +189,7 @@ export const MarkdownEditor = forwardRef< clearAll(); }; - }, [appendFile, cleanupUnreferenced, clearAll, setMarkdown]); + }, [appendFile, cleanupUnreferenced, clearAll, defaultMarkdown, setMarkdown]); return (
diff --git a/messages/en.json b/messages/en.json index 2d7148b8..578f8920 100644 --- a/messages/en.json +++ b/messages/en.json @@ -160,8 +160,39 @@ } }, "editor": { - "pageTitle": "Article Editor", - "heading": "Editor" + "pageTitle": "Write an Article", + "heading": "Editor", + "pageSubtitle": "Publish directly, then submit to the knowledge base with one click if you wish.", + "backHome": "Back to Home", + "contentHeading": "Article Content", + "stats": "{characters} characters · {images} images", + "titleRequired": "Please enter a title", + "publishTo": "Will be published at", + "clearConfirm": "Are you sure you want to clear all content?", + "clear": "Clear", + "publishing": "Publishing...", + "publish": "Publish Article", + "directPublish": "Publish Directly", + "tips": { + "imageUpload": "Pasted images are uploaded to the CDN automatically; no extra work is needed when publishing.", + "publishImmediately": "Published articles are visible immediately and can be shared directly without review.", + "promoteToDocs": "Want to add it to the knowledge base? Submit it after publishing with one click." + }, + "defaultMarkdown": "# Start writing...\n\nWrite your Markdown content here.\n\nYou can paste images!", + "autoSlug": "After publication, the slug is generated automatically from the title.", + "errors": { + "imageType": "Cannot determine the image type for {filename}. Save it as PNG, JPG, or WebP, then try again.", + "uploadLink": "Unable to get the upload URL.", + "imageUpload": "Image upload failed: {statusText}", + "titleRequired": "Please enter an article title.", + "invalidFilename": "The filename may contain only letters, numbers, hyphens, or underscores, and must start with a letter or number. Spaces and special characters have been cleaned automatically.", + "editorNotReady": "The editor is not ready, so images cannot be uploaded.", + "loginRequired": "Please sign in before publishing.", + "publishFailedHttp": "Publishing failed (HTTP {status}).", + "publishFailedRetry": "Publishing failed. Please try again.", + "publishFailed": "Publishing failed: {message}", + "unknownError": "Unknown error" + } }, "contribute": { "button": "Submit Contribution", diff --git a/messages/zh.json b/messages/zh.json index 7b935f7a..427849de 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -160,8 +160,39 @@ } }, "editor": { - "pageTitle": "文章编辑器", - "heading": "编辑器" + "pageTitle": "写篇文章", + "heading": "编辑器", + "pageSubtitle": "写完直接发布,想进知识库再一键投稿。", + "backHome": "返回首页", + "contentHeading": "文章内容", + "stats": "{characters} 字符 · {images} 张图片", + "titleRequired": "请填写标题", + "publishTo": "将发布到", + "clearConfirm": "确定要清空所有内容吗?", + "clear": "清空", + "publishing": "发布中...", + "publish": "发布文章", + "directPublish": "写完直接发", + "tips": { + "imageUpload": "图片粘贴后自动上传到 CDN,发布时无需额外处理", + "publishImmediately": "发布即可见,链接可直接分享,不等 review", + "promoteToDocs": "想进知识库?发布后点「收录进知识库」一键投稿" + }, + "defaultMarkdown": "# 开始写作...\n\n在这里输入你的 Markdown 内容。\n\n支持粘贴图片!", + "autoSlug": "发布后 slug 由标题自动生成", + "errors": { + "imageType": "无法识别图片类型:{filename}(浏览器未给出 MIME),请另存为 PNG/JPG/WebP 后重试", + "uploadLink": "获取上传链接失败", + "imageUpload": "上传图片失败:{statusText}", + "titleRequired": "请输入文章标题", + "invalidFilename": "文件名仅支持字母、数字、连字符或下划线,并需以字母或数字开头(已自动清洗空格和特殊符号)。", + "editorNotReady": "编辑器尚未就绪,无法上传图片", + "loginRequired": "请先登录后再发布", + "publishFailedHttp": "发布失败(HTTP {status})", + "publishFailedRetry": "发布失败,请重试", + "publishFailed": "发布失败:{message}", + "unknownError": "未知错误" + } }, "contribute": { "button": "投稿", From edb20ac3f8fedeb340e10db35eddfb67d8a413b3 Mon Sep 17 00:00:00 2001 From: Richard Sun <1464315342s@gmail.com> Date: Sat, 25 Jul 2026 17:01:59 +1000 Subject: [PATCH 2/2] chore(i18n): clean up editor translations --- messages/en.json | 3 +-- messages/zh.json | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/messages/en.json b/messages/en.json index 578f8920..0032ff9b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -161,7 +161,6 @@ }, "editor": { "pageTitle": "Write an Article", - "heading": "Editor", "pageSubtitle": "Publish directly, then submit to the knowledge base with one click if you wish.", "backHome": "Back to Home", "contentHeading": "Article Content", @@ -181,7 +180,7 @@ "defaultMarkdown": "# Start writing...\n\nWrite your Markdown content here.\n\nYou can paste images!", "autoSlug": "After publication, the slug is generated automatically from the title.", "errors": { - "imageType": "Cannot determine the image type for {filename}. Save it as PNG, JPG, or WebP, then try again.", + "imageType": "Cannot determine the image type for {filename} (the browser did not provide a MIME type). Save it as PNG, JPG, or WebP, then try again.", "uploadLink": "Unable to get the upload URL.", "imageUpload": "Image upload failed: {statusText}", "titleRequired": "Please enter an article title.", diff --git a/messages/zh.json b/messages/zh.json index 427849de..905b3197 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -161,7 +161,6 @@ }, "editor": { "pageTitle": "写篇文章", - "heading": "编辑器", "pageSubtitle": "写完直接发布,想进知识库再一键投稿。", "backHome": "返回首页", "contentHeading": "文章内容",