diff --git a/apps/extension/README.md b/apps/extension/README.md index 499ffa94..45b11927 100644 --- a/apps/extension/README.md +++ b/apps/extension/README.md @@ -12,6 +12,12 @@ Chrome/Edge Manifest V3 extension for saving the current webpage or selected tex The extension does not use a central relay service. The page content is sent directly to the EdgeEver instance configured by the user. +## Localization + +The extension uses Chrome's native `chrome.i18n` support. English is the fallback locale, and Simplified Chinese is available for browsers whose UI language is Chinese. + +User-visible strings live in `public/_locales//messages.json`. Add or update every supported locale when changing interface copy. Chrome Web Store listing translations are maintained separately in the developer dashboard; ready-to-paste English and Simplified Chinese copy is available in `STORE_LISTING.md`. + ## Development From the repository root: diff --git a/apps/extension/STORE_LISTING.md b/apps/extension/STORE_LISTING.md index 8edc3dcb..477833a5 100644 --- a/apps/extension/STORE_LISTING.md +++ b/apps/extension/STORE_LISTING.md @@ -9,6 +9,13 @@ - Support URL: `https://github.com/tianma-if/edgeever/issues` - Privacy policy: `https://edgeever.org/privacy` +## Localized listings + +- `中文(简体)`: Primary language +- `English`: Localized listing + +Select the matching language in the Chrome Web Store developer dashboard and enter the corresponding copy below. Store listing localization is separate from the extension's packaged `_locales` messages. + ## Upload files - Package: `store-assets/edgeever-web-clipper-v0.1.0.zip` @@ -16,11 +23,13 @@ - Screenshot: `store-assets/screenshot-options-1280x800.jpg` - Small promo tile: `store-assets/promo-small-440x280.jpg` -### Summary +### 中文(简体) + +#### Summary 将当前网页或选中内容保存到你自托管的 EdgeEver 实例。 -### Detailed description +#### Detailed description EdgeEver Web Clipper 可以把当前网页或你选中的内容保存到自托管的 EdgeEver 实例。 @@ -36,6 +45,28 @@ EdgeEver Web Clipper 可以把当前网页或你选中的内容保存到自托 EdgeEver 是开源、自托管的现代笔记工作区。项目主页与源代码:https://github.com/tianma-if/edgeever +### English + +#### Summary + +Save the current webpage or selected content to your self-hosted EdgeEver instance. + +#### Detailed description + +EdgeEver Web Clipper saves the current webpage or selected content to your self-hosted EdgeEver instance. + +Key features: + +- Extract article content automatically and convert it to searchable, editable Markdown. +- Prefer content selected on the page when a selection is available. +- Preserve the original title, source URL, and clipping time in the note. +- Select a default notebook and add the `web-clip` tag automatically. +- Send webpage content directly to your configured EdgeEver instance without a developer-operated relay server. + +Before using the extension, enter your EdgeEver instance URL and API token in the extension settings. The extension reads the current tab only after you click “Save current page” and requests network access only for the EdgeEver instance you authorize. + +EdgeEver is an open-source, self-hosted modern notes workspace. Project homepage and source code: https://github.com/tianma-if/edgeever + ## Privacy practices ### Single purpose diff --git a/apps/extension/options.html b/apps/extension/options.html index 24e9b71b..b8512d5c 100644 --- a/apps/extension/options.html +++ b/apps/extension/options.html @@ -1,36 +1,36 @@ - + - EdgeEver Web Clipper 设置 + EdgeEver Web Clipper settings
-

EdgeEver Web Clipper

+

EdgeEver Web Clipper

- - + +

diff --git a/apps/extension/popup.html b/apps/extension/popup.html index 7d38fc69..b6f0142f 100644 --- a/apps/extension/popup.html +++ b/apps/extension/popup.html @@ -1,9 +1,9 @@ - + - 保存到 EdgeEver + Save to EdgeEver
@@ -11,16 +11,16 @@

EdgeEver

-

网页剪藏

+

Web Clipper

- +

diff --git a/apps/extension/public/_locales/en/messages.json b/apps/extension/public/_locales/en/messages.json new file mode 100644 index 00000000..795356e7 --- /dev/null +++ b/apps/extension/public/_locales/en/messages.json @@ -0,0 +1,114 @@ +{ + "extensionName": { + "message": "EdgeEver Web Clipper" + }, + "extensionDescription": { + "message": "Save the current webpage or selected content to your self-hosted EdgeEver instance." + }, + "actionTitle": { + "message": "Save to EdgeEver" + }, + "webClipper": { + "message": "Web Clipper" + }, + "popupDescription": { + "message": "Save the current webpage or selected content to your EdgeEver." + }, + "saveCurrentPage": { + "message": "Save current page" + }, + "pluginSettings": { + "message": "Extension settings" + }, + "readingAndSaving": { + "message": "Reading and saving the webpage…" + }, + "completePluginConfiguration": { + "message": "Please complete the extension setup" + }, + "savedToEdgeEver": { + "message": "Saved to EdgeEver." + }, + "saveFailed": { + "message": "Failed to save." + }, + "optionsTitle": { + "message": "EdgeEver Web Clipper settings" + }, + "instanceAddress": { + "message": "EdgeEver instance URL" + }, + "tokenPlaceholder": { + "message": "Create one in EdgeEver settings" + }, + "defaultNotebook": { + "message": "Default notebook" + }, + "autoFirstNotebook": { + "message": "Use the first notebook automatically after connecting" + }, + "testConnection": { + "message": "Test connection" + }, + "saveSettings": { + "message": "Save settings" + }, + "privacyPolicy": { + "message": "Privacy policy" + }, + "connecting": { + "message": "Connecting…" + }, + "connectionSuccessOne": { + "message": "Connected. Found 1 notebook." + }, + "connectionSuccessMany": { + "message": "Connected. Found $COUNT$ notebooks.", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "connectionFailed": { + "message": "Connection failed." + }, + "settingsSaved": { + "message": "Settings saved." + }, + "settingsSaveFailed": { + "message": "Failed to save settings." + }, + "missingSettingsDetails": { + "message": "Enter the EdgeEver instance URL and API token in the extension settings first." + }, + "instancePermissionRequired": { + "message": "Allow the extension to access your EdgeEver instance." + }, + "requestFailed": { + "message": "EdgeEver request failed ($STATUS$).", + "placeholders": { + "status": { + "content": "$1" + } + } + }, + "sourceLabel": { + "message": "Source" + }, + "capturedAtLabel": { + "message": "Captured at" + }, + "noAvailableNotebooks": { + "message": "No notebooks are available in EdgeEver." + }, + "currentPageNotFound": { + "message": "Could not find the current page." + }, + "captureTimeout": { + "message": "Page extraction timed out. Please try again." + }, + "emptyPageContent": { + "message": "(No readable content was found on this page)" + } +} diff --git a/apps/extension/public/_locales/zh_CN/messages.json b/apps/extension/public/_locales/zh_CN/messages.json new file mode 100644 index 00000000..d2963d11 --- /dev/null +++ b/apps/extension/public/_locales/zh_CN/messages.json @@ -0,0 +1,114 @@ +{ + "extensionName": { + "message": "EdgeEver 网页剪藏" + }, + "extensionDescription": { + "message": "将当前网页或选中的内容保存到你自托管的 EdgeEver 实例。" + }, + "actionTitle": { + "message": "保存到 EdgeEver" + }, + "webClipper": { + "message": "网页剪藏" + }, + "popupDescription": { + "message": "保存当前网页或选中的内容到你的 EdgeEver。" + }, + "saveCurrentPage": { + "message": "保存当前网页" + }, + "pluginSettings": { + "message": "插件设置" + }, + "readingAndSaving": { + "message": "正在读取网页并保存……" + }, + "completePluginConfiguration": { + "message": "请先完成插件配置" + }, + "savedToEdgeEver": { + "message": "已保存到 EdgeEver。" + }, + "saveFailed": { + "message": "保存失败。" + }, + "optionsTitle": { + "message": "EdgeEver Web Clipper 设置" + }, + "instanceAddress": { + "message": "EdgeEver 实例地址" + }, + "tokenPlaceholder": { + "message": "从 EdgeEver 设置中创建" + }, + "defaultNotebook": { + "message": "默认笔记本" + }, + "autoFirstNotebook": { + "message": "连接后自动使用第一个笔记本" + }, + "testConnection": { + "message": "测试连接" + }, + "saveSettings": { + "message": "保存设置" + }, + "privacyPolicy": { + "message": "隐私政策" + }, + "connecting": { + "message": "正在连接……" + }, + "connectionSuccessOne": { + "message": "连接成功,共 1 个笔记本。" + }, + "connectionSuccessMany": { + "message": "连接成功,共 $COUNT$ 个笔记本。", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "connectionFailed": { + "message": "连接失败。" + }, + "settingsSaved": { + "message": "设置已保存。" + }, + "settingsSaveFailed": { + "message": "保存失败。" + }, + "missingSettingsDetails": { + "message": "请先在插件设置中填写 EdgeEver 地址和 API Token。" + }, + "instancePermissionRequired": { + "message": "需要允许插件访问你的 EdgeEver 实例。" + }, + "requestFailed": { + "message": "EdgeEver 请求失败($STATUS$)。", + "placeholders": { + "status": { + "content": "$1" + } + } + }, + "sourceLabel": { + "message": "来源" + }, + "capturedAtLabel": { + "message": "抓取时间" + }, + "noAvailableNotebooks": { + "message": "EdgeEver 中没有可用的笔记本。" + }, + "currentPageNotFound": { + "message": "找不到当前页面。" + }, + "captureTimeout": { + "message": "网页内容提取超时,请重试。" + }, + "emptyPageContent": { + "message": "(网页没有可提取的正文内容)" + } +} diff --git a/apps/extension/public/manifest.json b/apps/extension/public/manifest.json index ce190e33..def10c7c 100644 --- a/apps/extension/public/manifest.json +++ b/apps/extension/public/manifest.json @@ -1,8 +1,9 @@ { "manifest_version": 3, - "name": "EdgeEver Web Clipper", + "name": "__MSG_extensionName__", "version": "0.1.0", - "description": "Save the current webpage or selected content to your self-hosted EdgeEver instance.", + "description": "__MSG_extensionDescription__", + "default_locale": "en", "icons": { "16": "icons/icon-16.png", "32": "icons/icon-32.png", @@ -12,7 +13,7 @@ "permissions": ["activeTab", "scripting", "storage"], "optional_host_permissions": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"], "action": { - "default_title": "Save to EdgeEver", + "default_title": "__MSG_actionTitle__", "default_icon": { "16": "icons/icon-16.png", "32": "icons/icon-32.png" diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts index c64185fa..5663b36c 100644 --- a/apps/extension/src/background.ts +++ b/apps/extension/src/background.ts @@ -4,6 +4,7 @@ import { listNotebooks, type ExtensionSettings, } from "./extension"; +import { t } from "./i18n"; type CapturedPage = { title: string; @@ -13,14 +14,14 @@ type CapturedPage = { const toMarkdown = (page: CapturedPage) => { const capturedAt = new Date().toISOString(); - return `# ${page.title.replace(/\n/g, " ")}\n\n来源:[${page.url}](${page.url})\n\n抓取时间:${capturedAt}\n\n---\n\n${page.markdown}`; + return `# ${page.title.replace(/\n/g, " ")}\n\n${t("sourceLabel")}: [${page.url}](${page.url})\n\n${t("capturedAtLabel")}: ${capturedAt}\n\n---\n\n${page.markdown}`; }; const createMemo = async (settings: ExtensionSettings, page: CapturedPage) => { const notebooks = await listNotebooks(settings); const notebookId = settings.notebookId || notebooks.notebooks[0]?.id; if (!notebookId) { - throw new Error("EdgeEver 中没有可用的笔记本。"); + throw new Error(t("noAvailableNotebooks")); } await edgeEverRequest(settings, "/api/v1/memos", { @@ -50,7 +51,7 @@ chrome.runtime.onMessage.addListener((message: { type?: string; page?: CapturedP const notebooks = await listNotebooks(settings); sendResponse({ ok: true, notebooks: notebooks.notebooks }); } catch (error) { - sendResponse({ ok: false, message: error instanceof Error ? error.message : "连接失败。" }); + sendResponse({ ok: false, message: error instanceof Error ? error.message : t("connectionFailed") }); } })(); return true; @@ -62,13 +63,13 @@ chrome.runtime.onMessage.addListener((message: { type?: string; page?: CapturedP const settings = await getSettings(); const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab?.id) { - throw new Error("找不到当前页面。"); + throw new Error(t("currentPageNotFound")); } const page = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { pendingCapture = null; - reject(new Error("网页内容提取超时,请重试。")); + reject(new Error(t("captureTimeout"))); }, 15_000); pendingCapture = (capturedPage) => { @@ -88,7 +89,7 @@ chrome.runtime.onMessage.addListener((message: { type?: string; page?: CapturedP await createMemo(settings, page); sendResponse({ ok: true }); } catch (error) { - sendResponse({ ok: false, message: error instanceof Error ? error.message : "保存失败。" }); + sendResponse({ ok: false, message: error instanceof Error ? error.message : t("saveFailed") }); } })(); return true; diff --git a/apps/extension/src/capture.ts b/apps/extension/src/capture.ts index d11cc64c..0766decb 100644 --- a/apps/extension/src/capture.ts +++ b/apps/extension/src/capture.ts @@ -1,5 +1,6 @@ import { Readability } from "@mozilla/readability"; import TurndownService from "turndown"; +import { t } from "./i18n"; type CapturedPage = { title: string; @@ -43,7 +44,7 @@ const capturePage = (): CapturedPage => { return { title, url: location.href, - markdown: markdown || "(网页没有可提取的正文内容)", + markdown: markdown || t("emptyPageContent"), }; }; diff --git a/apps/extension/src/extension.ts b/apps/extension/src/extension.ts index 4ffc5077..5aeddf7b 100644 --- a/apps/extension/src/extension.ts +++ b/apps/extension/src/extension.ts @@ -1,3 +1,5 @@ +import { t } from "./i18n"; + export type ExtensionSettings = { instanceUrl: string; token: string; @@ -47,14 +49,14 @@ export const requestInstancePermission = async (instanceUrl: string) => { const origin = getInstanceOrigin(instanceUrl); const granted = await chrome.permissions.request({ origins: [`${origin}/*`] }); if (!granted) { - throw new Error("需要允许插件访问你的 EdgeEver 实例。"); + throw new Error(t("instancePermissionRequired")); } }; export const edgeEverRequest = async (settings: ExtensionSettings, path: string, init?: RequestInit): Promise => { const instanceUrl = normalizeInstanceUrl(settings.instanceUrl); if (!instanceUrl || !settings.token) { - throw new Error("请先在插件设置中填写 EdgeEver 地址和 API Token。"); + throw new Error(t("missingSettingsDetails")); } const response = await fetch(`${instanceUrl}${path}`, { @@ -68,7 +70,7 @@ export const edgeEverRequest = async (settings: ExtensionSettings, path: stri if (!response.ok) { const body = await response.json().catch(() => null) as { error?: { message?: string } } | null; - throw new Error(body?.error?.message || `EdgeEver 请求失败(${response.status})。`); + throw new Error(body?.error?.message || t("requestFailed", String(response.status))); } return response.json() as Promise; diff --git a/apps/extension/src/i18n.ts b/apps/extension/src/i18n.ts new file mode 100644 index 00000000..851e9066 --- /dev/null +++ b/apps/extension/src/i18n.ts @@ -0,0 +1,24 @@ +type MessageSubstitutions = string | string[]; + +export const t = (key: string, substitutions?: MessageSubstitutions) => + chrome.i18n.getMessage(key, substitutions) || key; + +export const localizeDocument = () => { + const locale = chrome.i18n.getMessage("@@ui_locale") || chrome.i18n.getUILanguage(); + document.documentElement.lang = locale.replace(/_/g, "-"); + document.documentElement.dir = chrome.i18n.getMessage("@@bidi_dir") || "ltr"; + + for (const element of document.querySelectorAll("[data-i18n]")) { + const key = element.dataset.i18n; + if (key) { + element.textContent = t(key); + } + } + + for (const element of document.querySelectorAll("[data-i18n-placeholder]")) { + const key = element.dataset.i18nPlaceholder; + if (key) { + element.placeholder = t(key); + } + } +}; diff --git a/apps/extension/src/options.ts b/apps/extension/src/options.ts index 0206e6d6..a637ece7 100644 --- a/apps/extension/src/options.ts +++ b/apps/extension/src/options.ts @@ -1,5 +1,8 @@ import "./styles.css"; import { getSettings, saveSettings, testConnection, type ExtensionSettings } from "./extension"; +import { localizeDocument, t } from "./i18n"; + +localizeDocument(); type NotebookSelect = { value: string; @@ -32,7 +35,7 @@ const renderNotebooks = (notebooks: Array<{ id: string; name: string }>, selecte return; } - notebookSelect.replaceChildren(new Option("连接后自动使用第一个笔记本", "")); + notebookSelect.replaceChildren(new Option(t("autoFirstNotebook"), "")); for (const notebook of notebooks) { notebookSelect.add(new Option(notebook.name, notebook.id)); } @@ -48,14 +51,15 @@ const initialize = async () => { testButton?.addEventListener("click", async () => { testButton.disabled = true; - setStatus("正在连接……"); + setStatus(t("connecting")); try { const settings = readSettings(); const notebooks = await testConnection(settings); renderNotebooks(notebooks, notebookSelect?.value ?? ""); - setStatus(`连接成功,共 ${notebooks.length} 个笔记本。`, "success"); + const messageKey = notebooks.length === 1 ? "connectionSuccessOne" : "connectionSuccessMany"; + setStatus(t(messageKey, String(notebooks.length)), "success"); } catch (error) { - setStatus(error instanceof Error ? error.message : "连接失败。", "error"); + setStatus(error instanceof Error ? error.message : t("connectionFailed"), "error"); } finally { testButton.disabled = false; } @@ -65,9 +69,9 @@ form?.addEventListener("submit", async (event) => { event.preventDefault(); try { await saveSettings(readSettings()); - setStatus("设置已保存。", "success"); + setStatus(t("settingsSaved"), "success"); } catch (error) { - setStatus(error instanceof Error ? error.message : "保存失败。", "error"); + setStatus(error instanceof Error ? error.message : t("settingsSaveFailed"), "error"); } }); diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index 1d34a299..4dab6858 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -1,5 +1,8 @@ import "./styles.css"; import { getSettings, requestInstancePermission } from "./extension"; +import { localizeDocument, t } from "./i18n"; + +localizeDocument(); const saveButton = document.querySelector("#save"); const settingsButton = document.querySelector("#settings"); @@ -14,23 +17,23 @@ const setStatus = (message: string, kind: "normal" | "error" | "success" = "norm saveButton?.addEventListener("click", async () => { saveButton.disabled = true; - setStatus("正在读取网页并保存……"); + setStatus(t("readingAndSaving")); try { const settings = await getSettings(); if (!settings.instanceUrl || !settings.token) { - throw new Error("请先打开插件设置。"); + throw new Error(t("completePluginConfiguration")); } await requestInstancePermission(settings.instanceUrl); const response = await chrome.runtime.sendMessage({ type: "captureCurrentPage" }); if (!response?.ok) { - throw new Error(response?.message || "保存失败。"); + throw new Error(response?.message || t("saveFailed")); } - setStatus("已保存到 EdgeEver。", "success"); + setStatus(t("savedToEdgeEver"), "success"); } catch (error) { - setStatus(error instanceof Error ? error.message : "保存失败。", "error"); + setStatus(error instanceof Error ? error.message : t("saveFailed"), "error"); } finally { saveButton.disabled = false; }