Skip to content

Commit e892266

Browse files
committed
fix(i18n): localize article editor page
1 parent 618f71f commit e892266

4 files changed

Lines changed: 116 additions & 40 deletions

File tree

app/[locale]/editor/EditorPageClient.tsx

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Button } from "@/app/components/ui/button";
1010
import { useCallback, useRef, useState } from "react";
1111
import { useRouter } from "next/navigation";
1212
import Link from "next/link";
13+
import { useTranslations } from "next-intl";
1314
import type { UserView } from "@/lib/use-auth";
1415
import type { PostRequest, ApiResponse, PostView } from "@/app/types/post";
1516
import {
@@ -38,6 +39,7 @@ function titleToSlug(title: string): string {
3839

3940
export function EditorPageClient({ user }: EditorPageClientProps) {
4041
const router = useRouter();
42+
const t = useTranslations("editor");
4143
const [isPublishing, setIsPublishing] = useState(false);
4244
const [imageCount, setImageCount] = useState(0);
4345
const editorRef = useRef<MarkdownEditorHandle | null>(null);
@@ -61,9 +63,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
6163
// 否则 R2 返 403 SignatureDoesNotMatch。
6264
const primaryMime = file.type.split(";")[0]!.trim().toLowerCase();
6365
if (!primaryMime) {
64-
throw new Error(
65-
`无法识别图片类型:${file.name}(浏览器未给出 MIME),请另存为 PNG/JPG/WebP 后重试`,
66-
);
66+
throw new Error(t("errors.imageType", { filename: file.name }));
6767
}
6868

6969
const token = localStorage.getItem("satoken") ?? "";
@@ -83,7 +83,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
8383

8484
if (!response.ok) {
8585
const error = await response.json();
86-
throw new Error(error.error || "获取上传链接失败");
86+
throw new Error(error.error || t("errors.uploadLink"));
8787
}
8888

8989
const { uploadUrl, publicUrl } = await response.json();
@@ -96,7 +96,9 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
9696
});
9797

9898
if (!uploadResponse.ok) {
99-
throw new Error(`上传图片失败: ${uploadResponse.statusText}`);
99+
throw new Error(
100+
t("errors.imageUpload", { statusText: uploadResponse.statusText }),
101+
);
100102
}
101103

102104
return { blobUrl, publicUrl };
@@ -107,7 +109,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
107109

108110
try {
109111
if (!title.trim()) {
110-
alert("请输入文章标题");
112+
alert(t("errors.titleRequired"));
111113
return;
112114
}
113115

@@ -117,9 +119,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
117119
: titleToSlug(title);
118120

119121
if (rawSlug && !FILENAME_PATTERN.test(rawSlug)) {
120-
alert(
121-
"文件名仅支持字母、数字、连字符或下划线,并需以字母或数字开头(已自动清洗空格和特殊符号)。",
122-
);
122+
alert(t("errors.invalidFilename"));
123123
return;
124124
}
125125

@@ -133,7 +133,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
133133

134134
const editorHandle = editorRef.current;
135135
if (!editorHandle) {
136-
throw new Error("编辑器尚未就绪,无法上传图片");
136+
throw new Error(t("errors.editorNotReady"));
137137
}
138138

139139
// 清理编辑器中未被 Markdown 正文引用的孤儿图片
@@ -158,7 +158,7 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
158158

159159
const token = localStorage.getItem("satoken") ?? "";
160160
if (!token) {
161-
throw new Error("请先登录后再发布");
161+
throw new Error(t("errors.loginRequired"));
162162
}
163163

164164
const postRequest: PostRequest = {
@@ -186,20 +186,25 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
186186
const body = await res.json().catch(() => ({}));
187187
throw new Error(
188188
(body as { message?: string }).message ??
189-
`发布失败(HTTP ${res.status})`,
189+
t("errors.publishFailedHttp", { status: res.status }),
190190
);
191191
}
192192

193193
const body = (await res.json()) as ApiResponse<PostView>;
194194
if (!body.success || !body.data) {
195-
throw new Error(body.message ?? "发布失败,请重试");
195+
throw new Error(body.message ?? t("errors.publishFailedRetry"));
196196
}
197197

198198
const { slug: finalSlug, authorUsername } = body.data;
199199
router.push(`/u/${authorUsername}/posts/${finalSlug}`);
200200
} catch (error) {
201201
console.error("发布失败:", error);
202-
alert(`发布失败:${error instanceof Error ? error.message : "未知错误"}`);
202+
alert(
203+
t("errors.publishFailed", {
204+
message:
205+
error instanceof Error ? error.message : t("errors.unknownError"),
206+
}),
207+
);
203208
} finally {
204209
setIsPublishing(false);
205210
}
@@ -212,13 +217,11 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
212217
{/* 头部 */}
213218
<header className="mb-8 flex items-center justify-between">
214219
<div>
215-
<h1 className="text-3xl font-bold">写篇文章</h1>
216-
<p className="text-muted-foreground mt-1">
217-
写完直接发布,想进知识库再一键投稿。
218-
</p>
220+
<h1 className="text-3xl font-bold">{t("pageTitle")}</h1>
221+
<p className="text-muted-foreground mt-1">{t("pageSubtitle")}</p>
219222
</div>
220223
<Link href="/">
221-
<Button variant="outline">返回首页</Button>
224+
<Button variant="outline">{t("backHome")}</Button>
222225
</Link>
223226
</header>
224227

@@ -230,60 +233,61 @@ export function EditorPageClient({ user }: EditorPageClientProps) {
230233
{/* Markdown 编辑器 */}
231234
<div>
232235
<div className="mb-2 flex items-center justify-between">
233-
<h2 className="text-lg font-semibold">文章内容</h2>
236+
<h2 className="text-lg font-semibold">{t("contentHeading")}</h2>
234237
<div className="text-sm text-muted-foreground">
235-
{markdown.length} 字符 · {imageCount} 张图片
238+
{t("stats", { characters: markdown.length, images: imageCount })}
236239
</div>
237240
</div>
238241
<MarkdownEditor
239242
ref={editorRef}
240243
onImagesChange={handleImageCountChange}
244+
defaultMarkdown={t("defaultMarkdown")}
241245
/>
242246
</div>
243247

244248
{/* 操作区 */}
245249
<div className="flex items-center justify-between rounded-lg border border-border bg-card p-4">
246250
<div className="text-sm text-muted-foreground">
247251
{!title.trim() ? (
248-
<span className="text-destructive">请填写标题</span>
252+
<span className="text-destructive">{t("titleRequired")}</span>
249253
) : previewSlug ? (
250254
<span>
251-
将发布到{" "}
255+
{t("publishTo")}{" "}
252256
<code className="font-mono text-foreground">
253257
/u/{user.username}/posts/{previewSlug}
254258
</code>
255259
</span>
256260
) : (
257-
<span>发布后 slug 由标题自动生成</span>
261+
<span>{t("autoSlug")}</span>
258262
)}
259263
</div>
260264

261265
<div className="flex gap-2">
262266
<Button
263267
variant="outline"
264268
onClick={() => {
265-
if (confirm("确定要清空所有内容吗?")) {
269+
if (confirm(t("clearConfirm"))) {
266270
useEditorStore.getState().reset();
267271
window.location.reload();
268272
}
269273
}}
270274
>
271-
清空
275+
{t("clear")}
272276
</Button>
273277

274278
<Button onClick={handlePublish} disabled={!canPublish}>
275-
{isPublishing ? "发布中..." : "发布文章"}
279+
{isPublishing ? t("publishing") : t("publish")}
276280
</Button>
277281
</div>
278282
</div>
279283

280284
{/* 流程提示 */}
281285
<div className="rounded-lg border border-green-200 bg-green-50 p-4 text-sm dark:border-green-900 dark:bg-green-950">
282-
<h3 className="font-medium mb-2">写完直接发</h3>
286+
<h3 className="font-medium mb-2">{t("directPublish")}</h3>
283287
<ul className="space-y-1 text-muted-foreground list-disc list-inside">
284-
<li>图片粘贴后自动上传到 CDN,发布时无需额外处理</li>
285-
<li>发布即可见,链接可直接分享,不等 review</li>
286-
<li>想进知识库?发布后点「收录进知识库」一键投稿</li>
288+
<li>{t("tips.imageUpload")}</li>
289+
<li>{t("tips.publishImmediately")}</li>
290+
<li>{t("tips.promoteToDocs")}</li>
287291
</ul>
288292
</div>
289293
</div>

app/components/MarkdownEditor.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import "@milkdown/crepe/theme/frame.css";
2323

2424
interface MarkdownEditorProps {
2525
onImagesChange?: (count: number) => void;
26+
defaultMarkdown: string;
2627
}
2728

2829
export interface MarkdownEditorHandle {
@@ -42,11 +43,12 @@ export interface MarkdownEditorHandle {
4243
export const MarkdownEditor = forwardRef<
4344
MarkdownEditorHandle,
4445
MarkdownEditorProps
45-
>(function MarkdownEditor({ onImagesChange }, ref) {
46+
>(function MarkdownEditor({ onImagesChange, defaultMarkdown }, ref) {
4647
const editorRef = useRef<HTMLDivElement>(null);
4748
const crepeInstanceRef = useRef<Crepe | null>(null);
4849
const isLoadingRef = useRef(false);
4950
const markdownRef = useRef(useEditorStore.getState().markdown);
51+
const lastDefaultMarkdownRef = useRef<string | null>(null);
5052
const { markdown, setMarkdown } = useEditorStore();
5153
const { appendFile, cleanupUnreferenced, clearAll, getSnapshot } =
5254
useImageBuffer(onImagesChange);
@@ -110,11 +112,19 @@ export const MarkdownEditor = forwardRef<
110112
return nodes;
111113
};
112114

115+
// 默认示例也会同步到 store。切换语言时,只有内容仍等于上次注入的
116+
// 默认示例才替换为新语言;用户修改过的草稿必须保留。
117+
const shouldUseDefault =
118+
!markdownRef.current ||
119+
markdownRef.current === lastDefaultMarkdownRef.current;
120+
const initialMarkdown = shouldUseDefault
121+
? defaultMarkdown
122+
: markdownRef.current;
123+
lastDefaultMarkdownRef.current = initialMarkdown;
124+
113125
const crepe = new Crepe({
114126
root: editorRef.current!,
115-
defaultValue:
116-
markdownRef.current ||
117-
"# 开始写作...\n\n在这里输入你的 Markdown 内容。\n\n支持粘贴图片!",
127+
defaultValue: initialMarkdown,
118128
featureConfigs: {
119129
[Crepe.Feature.ImageBlock]: imageBlockConfig,
120130
},
@@ -179,7 +189,7 @@ export const MarkdownEditor = forwardRef<
179189

180190
clearAll();
181191
};
182-
}, [appendFile, cleanupUnreferenced, clearAll, setMarkdown]);
192+
}, [appendFile, cleanupUnreferenced, clearAll, defaultMarkdown, setMarkdown]);
183193

184194
return (
185195
<div className="rounded-lg border border-border bg-card shadow-sm overflow-visible">

messages/en.json

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,39 @@
160160
}
161161
},
162162
"editor": {
163-
"pageTitle": "Article Editor",
164-
"heading": "Editor"
163+
"pageTitle": "Write an Article",
164+
"heading": "Editor",
165+
"pageSubtitle": "Publish directly, then submit to the knowledge base with one click if you wish.",
166+
"backHome": "Back to Home",
167+
"contentHeading": "Article Content",
168+
"stats": "{characters} characters · {images} images",
169+
"titleRequired": "Please enter a title",
170+
"publishTo": "Will be published at",
171+
"clearConfirm": "Are you sure you want to clear all content?",
172+
"clear": "Clear",
173+
"publishing": "Publishing...",
174+
"publish": "Publish Article",
175+
"directPublish": "Publish Directly",
176+
"tips": {
177+
"imageUpload": "Pasted images are uploaded to the CDN automatically; no extra work is needed when publishing.",
178+
"publishImmediately": "Published articles are visible immediately and can be shared directly without review.",
179+
"promoteToDocs": "Want to add it to the knowledge base? Submit it after publishing with one click."
180+
},
181+
"defaultMarkdown": "# Start writing...\n\nWrite your Markdown content here.\n\nYou can paste images!",
182+
"autoSlug": "After publication, the slug is generated automatically from the title.",
183+
"errors": {
184+
"imageType": "Cannot determine the image type for {filename}. Save it as PNG, JPG, or WebP, then try again.",
185+
"uploadLink": "Unable to get the upload URL.",
186+
"imageUpload": "Image upload failed: {statusText}",
187+
"titleRequired": "Please enter an article title.",
188+
"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.",
189+
"editorNotReady": "The editor is not ready, so images cannot be uploaded.",
190+
"loginRequired": "Please sign in before publishing.",
191+
"publishFailedHttp": "Publishing failed (HTTP {status}).",
192+
"publishFailedRetry": "Publishing failed. Please try again.",
193+
"publishFailed": "Publishing failed: {message}",
194+
"unknownError": "Unknown error"
195+
}
165196
},
166197
"contribute": {
167198
"button": "Submit Contribution",

messages/zh.json

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,39 @@
160160
}
161161
},
162162
"editor": {
163-
"pageTitle": "文章编辑器",
164-
"heading": "编辑器"
163+
"pageTitle": "写篇文章",
164+
"heading": "编辑器",
165+
"pageSubtitle": "写完直接发布,想进知识库再一键投稿。",
166+
"backHome": "返回首页",
167+
"contentHeading": "文章内容",
168+
"stats": "{characters} 字符 · {images} 张图片",
169+
"titleRequired": "请填写标题",
170+
"publishTo": "将发布到",
171+
"clearConfirm": "确定要清空所有内容吗?",
172+
"clear": "清空",
173+
"publishing": "发布中...",
174+
"publish": "发布文章",
175+
"directPublish": "写完直接发",
176+
"tips": {
177+
"imageUpload": "图片粘贴后自动上传到 CDN,发布时无需额外处理",
178+
"publishImmediately": "发布即可见,链接可直接分享,不等 review",
179+
"promoteToDocs": "想进知识库?发布后点「收录进知识库」一键投稿"
180+
},
181+
"defaultMarkdown": "# 开始写作...\n\n在这里输入你的 Markdown 内容。\n\n支持粘贴图片!",
182+
"autoSlug": "发布后 slug 由标题自动生成",
183+
"errors": {
184+
"imageType": "无法识别图片类型:{filename}(浏览器未给出 MIME),请另存为 PNG/JPG/WebP 后重试",
185+
"uploadLink": "获取上传链接失败",
186+
"imageUpload": "上传图片失败:{statusText}",
187+
"titleRequired": "请输入文章标题",
188+
"invalidFilename": "文件名仅支持字母、数字、连字符或下划线,并需以字母或数字开头(已自动清洗空格和特殊符号)。",
189+
"editorNotReady": "编辑器尚未就绪,无法上传图片",
190+
"loginRequired": "请先登录后再发布",
191+
"publishFailedHttp": "发布失败(HTTP {status})",
192+
"publishFailedRetry": "发布失败,请重试",
193+
"publishFailed": "发布失败:{message}",
194+
"unknownError": "未知错误"
195+
}
165196
},
166197
"contribute": {
167198
"button": "投稿",

0 commit comments

Comments
 (0)