diff --git a/apps/mobile/src/hooks/useMobileRichEditor.ts b/apps/mobile/src/hooks/useMobileRichEditor.ts index 0322be02..e0ace052 100644 --- a/apps/mobile/src/hooks/useMobileRichEditor.ts +++ b/apps/mobile/src/hooks/useMobileRichEditor.ts @@ -20,7 +20,7 @@ import { } from "../lib/mobile-editor-controller"; import { clearMobileMemoDraft, writeMobileMemoDraft, type MobileMemoDraft } from "../lib/mobile-drafts"; import { prepareUploadAsset } from "../lib/mobile-image-upload"; -import { useMobileLocale } from "../lib/mobile-locale"; +import { localizeMissingNotebookName, useMobileLocale } from "../lib/mobile-locale"; import { useSession } from "../lib/session"; import { recordEditorStartup } from "../lib/startup-performance"; import { safeDomCall } from "../lib/safe-dom-call"; @@ -144,7 +144,7 @@ export const useMobileRichEditor = ({ const [error, setError] = useState(null); const [resourceTarget, setResourceTarget] = useState(null); const { pickUploadAssets, uploadSourcePicker } = useMobileEditorUploadAsset(); - const notebookLabel = notebooks.find((notebook) => notebook.id === notebookId)?.name ?? "未分类"; + const notebookLabel = notebooks.find((notebook) => notebook.id === notebookId)?.name ?? localizeMissingNotebookName(resolvedLocale); const saveLabel = error ? "保存失败" : saving diff --git a/apps/mobile/src/lib/mobile-locale.test.mjs b/apps/mobile/src/lib/mobile-locale.test.mjs index aecf0e94..e7947be8 100644 --- a/apps/mobile/src/lib/mobile-locale.test.mjs +++ b/apps/mobile/src/lib/mobile-locale.test.mjs @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { translateMobileText } from "./mobile-locale.tsx"; +import { localizeMissingNotebookName, localizeUntitledMemoTitle, translateMobileText } from "./mobile-locale.tsx"; describe("mobile locale translation", () => { test("keeps Chinese copy unchanged for the Chinese locale", () => { @@ -22,4 +22,27 @@ describe("mobile locale translation", () => { expect(translateMobileText("永久删除 3 条笔记", "en-US")).toBe("Delete 3 notes permanently"); expect(translateMobileText("当前:Inbox,3 条笔记", "en-US")).toBe("Current: Inbox, 3 notes"); }); + + test("translates the mixed English explanations from the Android locale report", () => { + expect(translateMobileText("修改后会保留当前设备登录,并退出其他设备上的登录会话。", "en-US")) + .toBe("Keeps this device signed in and signs out other sessions."); + expect(translateMobileText("修改后会保留当前设备登录,并退出其他设备上的登录会话。", "ja")) + .toBe("この端末のログインは維持し、他の端末のセッションは終了します。"); + expect(translateMobileText("添加标签", "en-US")).toBe("Add tags"); + expect(translateMobileText("没有找到适合这篇笔记的新标签。", "en-US")).toBe("No new tags fit this note."); + expect(translateMobileText("切换产品界面的显示语言。", "en-US")).toBe("Choose the language used in the app."); + expect(translateMobileText("当前密码不正确。", "en-US")).toBe("The current password is incorrect."); + expect(translateMobileText("修改中...", "ja")).toBe("変更中..."); + }); + + test("localizes only the default untitled title and the missing-notebook fallback", () => { + expect(localizeUntitledMemoTitle("", "en-US")).toBe("Untitled note"); + expect(localizeUntitledMemoTitle("无标题笔记", "en-US")).toBe("Untitled note"); + expect(localizeUntitledMemoTitle("无标题笔记", "ja")).toBe("無題のノート"); + expect(localizeUntitledMemoTitle(" 无标题笔记 ", "zh-CN")).toBe("无标题笔记"); + expect(localizeUntitledMemoTitle("会议记录", "en-US")).toBe("会议记录"); + expect(localizeMissingNotebookName("en-US")).toBe("Uncategorized"); + expect(localizeMissingNotebookName("ja")).toBe("未分類"); + expect(localizeMissingNotebookName("zh-CN")).toBe("未分类"); + }); }); diff --git a/apps/mobile/src/lib/mobile-locale.tsx b/apps/mobile/src/lib/mobile-locale.tsx index 606cede4..c71d02b5 100644 --- a/apps/mobile/src/lib/mobile-locale.tsx +++ b/apps/mobile/src/lib/mobile-locale.tsx @@ -1,3 +1,4 @@ +import { DEFAULT_MEMO_TITLE } from "@edgeever/shared"; import { enUS, ja, zhCN } from "@edgeever/shared/i18n"; import { resolveSupportedLocale } from "@edgeever/shared/i18n/locales"; import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; @@ -165,6 +166,21 @@ const mobileOnlyTranslations = new Map([ ["当前内容将被模板内容替换。", "The current content will be replaced by the template."], ["替换", "Replace"], ["关闭", "Close"], + ["修改后会保留当前设备登录,并退出其他设备上的登录会话。", "Keeps this device signed in and signs out other sessions."], + ["添加标签", "Add tags"], + ["正在生成智能标签", "Generating smart tags"], + ["智能标签生成失败", "Could not generate smart tags"], + ["智能标签已添加", "Smart tags added"], + ["没有找到适合这篇笔记的新标签。", "No new tags fit this note."], + ["没有匹配的笔记本", "No matching notebooks"], + ["偏好设置", "Preferences"], + ["切换产品界面的显示语言。", "Choose the language used in the app."], + ["上传大图时在本地压缩,节省资源占用。", "Compress large images on this device before upload."], + ["已恢复上次未完成的本地草稿", "Restored your unfinished local draft"], + ["连接你的自托管笔记空间", "Connect to your self-hosted notes"], + ["首次登录密码", "Password for first sign-in"], + ["GitHub 仓库", "GitHub repository"], + ["操作失败,请稍后再试", "Something went wrong. Please try again."], ]); const mobileOnlyJapanese = new Map([ @@ -314,6 +330,21 @@ const mobileOnlyJapanese = new Map([ ["当前内容将被模板内容替换。", "現在の内容はテンプレートの内容に置き換わります。"], ["替换", "置き換え"], ["关闭", "閉じる"], + ["修改后会保留当前设备登录,并退出其他设备上的登录会话。", "この端末のログインは維持し、他の端末のセッションは終了します。"], + ["添加标签", "タグを追加"], + ["正在生成智能标签", "スマートタグを生成しています"], + ["智能标签生成失败", "スマートタグを生成できませんでした"], + ["智能标签已添加", "スマートタグを追加しました"], + ["没有找到适合这篇笔记的新标签。", "このノートに合う新しいタグはありません。"], + ["没有匹配的笔记本", "一致するノートブックはありません"], + ["偏好设置", "環境設定"], + ["切换产品界面的显示语言。", "アプリの表示言語を切り替えます。"], + ["上传大图时在本地压缩,节省资源占用。", "大きな画像はアップロード前にこの端末で圧縮し、容量を抑えます。"], + ["已恢复上次未完成的本地草稿", "前回の未完成の下書きを復元しました"], + ["连接你的自托管笔记空间", "セルフホストのノートスペースに接続"], + ["首次登录密码", "初回ログインのパスワード"], + ["GitHub 仓库", "GitHub リポジトリ"], + ["操作失败,请稍后再试", "操作に失敗しました。しばらくしてから再試行してください。"], ]); const flattenStrings = (value: unknown, prefix = "", output = new Map()) => { @@ -416,6 +447,23 @@ export const translateMobileText = (value: string, locale: SupportedMobileLocale let currentResolvedMobileLocale: SupportedMobileLocale = resolveSystemLocale(); export const translateCurrentMobileText = (value: string) => translateMobileText(value, currentResolvedMobileLocale); +export const localizeUntitledMemoTitle = ( + title: string | null | undefined, + locale: SupportedMobileLocale, +) => { + const trimmed = title?.trim() ?? ""; + if (trimmed && trimmed !== DEFAULT_MEMO_TITLE) { + return trimmed; + } + return translateMobileText(DEFAULT_MEMO_TITLE, locale); +}; + +export const localizeMissingNotebookName = (locale: SupportedMobileLocale) => { + if (locale === "en-US") return "Uncategorized"; + if (locale === "ja") return "未分類"; + return "未分类"; +}; + const MobileLocaleContext = createContext({ preference: "system", resolvedLocale: resolveSystemLocale(), diff --git a/apps/mobile/src/screens/AccountSecurityModal.tsx b/apps/mobile/src/screens/AccountSecurityModal.tsx index 1f230290..b0b16a52 100644 --- a/apps/mobile/src/screens/AccountSecurityModal.tsx +++ b/apps/mobile/src/screens/AccountSecurityModal.tsx @@ -25,8 +25,8 @@ export const AccountSecurityPanel = ({ const passwordMutation = useMutation({ mutationFn: async () => { if (!client) throw new Error("Client is not ready"); - if (newPassword.length < 8) throw new Error("新密码至少需要 8 个字符"); - if (newPassword !== confirmPassword) throw new Error("两次输入的新密码不一致"); + if (newPassword.length < 8) throw new Error("新密码至少需要 8 个字符。"); + if (newPassword !== confirmPassword) throw new Error("两次输入的新密码不一致。"); return client.changePassword({ currentPassword, newPassword, confirmPassword }); }, onSuccess: () => { @@ -43,7 +43,7 @@ export const AccountSecurityPanel = ({ }, [active]); const errorMessage = (error: unknown) => { - if (error instanceof ApiRequestError && error.code === "invalid_current_password") return "当前密码不正确"; + if (error instanceof ApiRequestError && error.code === "invalid_current_password") return "当前密码不正确。"; return error instanceof Error ? error.message : "操作失败,请稍后再试"; }; @@ -63,7 +63,7 @@ export const AccountSecurityPanel = ({ {passwordMutation.isSuccess ? 密码已修改成功。 : null} passwordMutation.mutate()} /> diff --git a/apps/mobile/src/screens/WorkspaceEditors.tsx b/apps/mobile/src/screens/WorkspaceEditors.tsx index 2c7fb77a..d06b0f67 100644 --- a/apps/mobile/src/screens/WorkspaceEditors.tsx +++ b/apps/mobile/src/screens/WorkspaceEditors.tsx @@ -22,7 +22,7 @@ import { writeMobileMemoDraft, type MobileMemoDraft, } from "../lib/mobile-drafts"; -import { useMobileLocale } from "../lib/mobile-locale"; +import { localizeMissingNotebookName, useMobileLocale } from "../lib/mobile-locale"; import { useSession } from "../lib/session"; import { queueMobileMemoCreate, queueMobileMemoUpdate } from "../lib/sync-queue"; import { createMobileDataScope, upsertLocalMemo } from "../lib/local-mirror"; @@ -867,7 +867,7 @@ export const RichEditorModal = ({ const [resourceTarget, setResourceTarget] = useState(null); const editorStartup = useMobileEditorStartupGuard({ active: Boolean(memo && baseUrl), ready }); const { pickUploadAssets, uploadSourcePicker } = useMobileEditorUploadAsset(); - const notebookLabel = notebooks.find((notebook) => notebook.id === notebookId)?.name ?? "未分类"; + const notebookLabel = notebooks.find((notebook) => notebook.id === notebookId)?.name ?? localizeMissingNotebookName(resolvedLocale); const saveLabel = error ? "保存失败" : saving ? "保存中" : uploading ? "上传中" : dirty ? (draftRestored ? "本地草稿" : "未保存") : ready ? "已保存" : "加载中"; const titleRef = useRef(title); const tagsTextRef = useRef(tagsText); diff --git a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx index 768a07a0..2c75ffd0 100644 --- a/apps/mobile/src/screens/WorkspaceMemoDetail.tsx +++ b/apps/mobile/src/screens/WorkspaceMemoDetail.tsx @@ -27,7 +27,7 @@ import { saveMobileResourceAs, type MobileResourceTarget, } from "../lib/mobile-attachments"; -import { useMobileLocale } from "../lib/mobile-locale"; +import { localizeUntitledMemoTitle, useMobileLocale } from "../lib/mobile-locale"; import { createOnceProtectedResourceFailureNotifier, isProtectedResourceSource, @@ -625,7 +625,7 @@ export const MemoDetailModal = ({ } }, []); - const memoTitle = memo?.title?.trim() || DEFAULT_MEMO_TITLE; + const memoTitle = localizeUntitledMemoTitle(memo?.title, resolvedLocale); const memoTagsText = memo?.tags.join(", ") ?? ""; const metadataSearchMatches = useMemo(() => ({ tags: getTextSearchMatches(memoTagsText, searchQuery), @@ -844,8 +844,8 @@ export const MemoDetailModal = ({ safeDomCall(() => viewerRef.current?.exportImage(JSON.stringify({ requestId, format, - title: memo.title?.trim() || (resolvedLocale !== "zh-CN" ? "Untitled note" : "无标题笔记"), - fallbackTitle: resolvedLocale !== "zh-CN" ? "Untitled note" : "无标题笔记", + title: localizeUntitledMemoTitle(memo.title, resolvedLocale), + fallbackTitle: localizeUntitledMemoTitle(null, resolvedLocale), notebook: options.showNotebook === false ? "" : notebookName, tags: options.showTags === false ? [] : memo.tags, updatedAt: options.showUpdatedAt === false ? "" : new Date(memo.updatedAt).toLocaleString(resolvedLocale), diff --git a/apps/mobile/src/screens/WorkspaceScreen.tsx b/apps/mobile/src/screens/WorkspaceScreen.tsx index 733e9fc5..777df86f 100644 --- a/apps/mobile/src/screens/WorkspaceScreen.tsx +++ b/apps/mobile/src/screens/WorkspaceScreen.tsx @@ -30,7 +30,7 @@ import { type MobileLocalePreference, type MobileMemoListDensity, } from "../lib/preferences"; -import { useMobileLocale } from "../lib/mobile-locale"; +import { localizeUntitledMemoTitle, localizeMissingNotebookName, useMobileLocale } from "../lib/mobile-locale"; import { useSession } from "../lib/session"; import { clearMobileMemoUpdateQueueItem, @@ -969,9 +969,10 @@ export const WorkspaceScreen = ({ const response = await client.createMemoShare(memo.id); const shareUrl = `${session.baseUrl.replace(/\/+$/, "")}/share/${encodeURIComponent(response.share.token)}`; + const shareTitle = localizeUntitledMemoTitle(memo.title, resolvedLocale); await NativeShare.share({ - message: `${memo.title?.trim() || DEFAULT_MEMO_TITLE}\n${shareUrl}`, - title: memo.title?.trim() || DEFAULT_MEMO_TITLE, + message: `${shareTitle}\n${shareUrl}`, + title: shareTitle, url: shareUrl, }); }, @@ -1277,7 +1278,7 @@ export const WorkspaceScreen = ({ isSaving={updateMemoMutation.isPending || localUpdateMemoMutation.isPending} isSharing={shareMemoMutation.isPending} memo={selectedMemo} - notebookName={notebooks.find((notebook) => notebook.id === selectedMemo?.notebookId)?.name ?? "未分类"} + notebookName={notebooks.find((notebook) => notebook.id === selectedMemo?.notebookId)?.name ?? localizeMissingNotebookName(resolvedLocale)} notebooks={notebooks} onClose={closeDetail} onCloseEditor={closeRichEditor}