From 54a038d92e69ae9768d579525d1293a87410acaf Mon Sep 17 00:00:00 2001 From: LiHaohua Date: Thu, 23 Jul 2026 18:21:03 +0800 Subject: [PATCH] feat(portal): i18n (zh-CN / en / ja) The developer portal (dev.cardputer.cc) now supports Simplified Chinese, English, and Japanese, matching the rest of cardputer.cc. - Add site/i18n/{zh-CN,en,ja}.json with all UI strings. - Add a language switcher in the header; choice persists in localStorage (dev.locale) and otherwise follows the browser language, defaulting to zh-CN. - app.js gains a small i18n runtime (loadLocale/t/applyStaticI18n) and all static + dynamic strings (preview, checks, store form, my-packages, status/error messages) are keyed. Switching language re-renders live. Co-authored-by: Cursor --- site/app.js | 147 ++++++++++++++++++++++++++++++++----------- site/i18n/en.json | 77 +++++++++++++++++++++++ site/i18n/ja.json | 77 +++++++++++++++++++++++ site/i18n/zh-CN.json | 77 +++++++++++++++++++++++ site/index.html | 79 ++++++++++++++--------- 5 files changed, 389 insertions(+), 68 deletions(-) create mode 100644 site/i18n/en.json create mode 100644 site/i18n/ja.json create mode 100644 site/i18n/zh-CN.json diff --git a/site/app.js b/site/app.js index 7f5c556..e71a938 100644 --- a/site/app.js +++ b/site/app.js @@ -38,6 +38,63 @@ let parsed = null; let file = null; let publishedIndex = null; +/* ---------------------------------- i18n --------------------------------- */ +// Mirrors the main cardputer.cc site: zh-CN / en / ja, choice persists in +// localStorage, falls back to the browser language then zh-CN. +const SUPPORTED_LOCALES = ["zh-CN", "en", "ja"]; +const LOCALE_KEY = "dev.locale"; +const I18N_TS = Date.now(); // cache-buster for the locale JSON on each load +let dict = {}; +let locale = resolveInitialLocale(); + +function resolveInitialLocale() { + const stored = localStorage.getItem(LOCALE_KEY); + if (SUPPORTED_LOCALES.includes(stored)) return stored; + const b = navigator.language || ""; + if (SUPPORTED_LOCALES.includes(b)) return b; + if (b.startsWith("zh")) return "zh-CN"; + if (b.startsWith("ja")) return "ja"; + if (b.startsWith("en")) return "en"; + return "zh-CN"; +} + +async function loadLocale(loc) { + try { + const r = await fetch(`/i18n/${loc}.json?t=${I18N_TS}`); + if (r.ok) return await r.json(); + } catch { /* fall through to zh-CN */ } + if (loc !== "zh-CN") { + try { return await (await fetch(`/i18n/zh-CN.json?t=${I18N_TS}`)).json(); } catch { /* offline */ } + } + return {}; +} + +// t("a.b", {name}) -> string with {name} interpolated; missing keys echo back. +function t(key, params = {}) { + let v = key.split(".").reduce((o, k) => (o == null ? o : o[k]), dict); + if (typeof v !== "string") v = key; + return Object.entries(params).reduce((s, [k, val]) => s.replaceAll(`{${k}}`, val), v); +} + +function applyStaticI18n() { + document.documentElement.lang = locale; + document.title = t("meta.title"); + document.querySelectorAll("[data-i18n]").forEach((n) => { n.textContent = t(n.dataset.i18n); }); + document.querySelectorAll("[data-i18n-html]").forEach((n) => { n.innerHTML = t(n.dataset.i18nHtml); }); + document.querySelectorAll("[data-i18n-ph]").forEach((n) => { n.placeholder = t(n.dataset.i18nPh); }); + document.querySelectorAll("[data-i18n-aria]").forEach((n) => { n.setAttribute("aria-label", t(n.dataset.i18nAria)); }); + const sel = $("locale-select"); + if (sel) sel.value = locale; +} + +// Re-apply everything after a language switch (static chrome + dynamic views). +function relocalize() { + applyStaticI18n(); + if (me) $("who").textContent = me.login + (me.is_admin ? t("header.admin") : ""); + if (parsed && file) renderPreview(); + if (!$("mine-panel").classList.contains("hidden")) renderMine(); +} + /* ------------------------------ decompressors ---------------------------- */ async function streamToBytes(stream) { @@ -61,7 +118,7 @@ async function loadOrExplain(loader, what) { impl = await loader(); } catch { /* fall through to the user-facing error below */ } if (!impl) { - throw new Error(`无法加载 ${what} 解压组件(仅用于本地预览),请刷新重试;仍可直接提交,服务器会完成校验`); + throw new Error(t("decompress.loadFailed", { what })); } return impl; } @@ -105,15 +162,28 @@ const decompressors = { /* --------------------------------- init ---------------------------------- */ async function init() { - try { - const r = await fetch("/api/me"); - if (r.ok) me = await r.json(); - } catch { /* not logged in */ } + // Fetch the session and the locale strings in parallel. + const meP = fetch("/api/me").then((r) => (r.ok ? r.json() : null)).catch(() => null); + dict = await loadLocale(locale); + applyStaticI18n(); + + const sel = $("locale-select"); + if (sel) { + sel.value = locale; + sel.addEventListener("change", async (e) => { + locale = e.target.value; + localStorage.setItem(LOCALE_KEY, locale); + dict = await loadLocale(locale); + relocalize(); + }); + } + + me = await meP; $("boot-view").classList.add("hidden"); $(me ? "app-view" : "login-view").classList.remove("hidden"); if (me) { $("who-box").classList.remove("hidden"); - $("who").textContent = me.login + (me.is_admin ? "(管理员)" : ""); + $("who").textContent = me.login + (me.is_admin ? t("header.admin") : ""); // Honor the tab encoded in the URL hash so a refresh stays put. applyRoute(); } @@ -186,17 +256,17 @@ $("file").addEventListener("change", (e) => pick(e.target.files[0])); async function pick(f) { if (!f) return; - if (!f.name.endsWith(".deb")) return say("err", "请选择 .deb 文件"); + if (!f.name.endsWith(".deb")) return say("err", t("upload.pickDeb")); file = f; parsed = null; - say("", "本地解析中…"); + say("", t("upload.localParsing")); $("preview-box").classList.add("hidden"); resetStoreForm(); try { const buf = await f.arrayBuffer(); parsed = await parseDeb(buf, decompressors); } catch (err) { - return say("err", `解析失败:${err.message}`); + return say("err", t("upload.parseFailed", { msg: err.message })); } say("", ""); await renderPreview(); @@ -206,15 +276,15 @@ async function pick(f) { async function renderPreview() { const c = parsed.control; - $("drop-text").innerHTML = `已选择 ${file.name}(${(file.size / 1048576).toFixed(1)} MB)— 点击可更换`; + $("drop-text").innerHTML = t("upload.chosen", { name: file.name, size: (file.size / 1048576).toFixed(1) }); $("p-name").textContent = (parsed.desktop && parsed.desktop.Name) || c.Package || "?"; $("p-pkg").textContent = c.Package || ""; $("p-version").textContent = c.Version || "?"; $("p-arch").textContent = c.Architecture || "?"; $("p-size").textContent = `${(parsed.totalInstalledSize / 1048576).toFixed(1)} MB`; - $("p-maint").textContent = c.Maintainer || "(缺失)"; + $("p-maint").textContent = c.Maintainer || t("preview.maintainerMissing"); - $("s-title").placeholder = (parsed.desktop && parsed.desktop.Name) || c.Package || "显示在 AppStore 里的名字"; + $("s-title").placeholder = (parsed.desktop && parsed.desktop.Name) || c.Package || t("preview.titlePlaceholder"); if (parsed.icon && parsed.icon.isPng) { const url = URL.createObjectURL(new Blob([parsed.icon.bytes], { type: "image/png" })); @@ -228,15 +298,15 @@ async function renderPreview() { // 上传者归属(包名先到先得,以 GitHub 账号为准,与 deb 里的 Maintainer 邮箱无关) $("p-emailmatch").innerHTML = me - ? `将以 @${me.login} 的身份记录为上传者` - : `请先登录 GitHub 再提交`; + ? `${t("preview.uploaderAs", { login: me.login })}` + : `${t("preview.needLogin")}`; // 版本 / 包名占用预检(所有权按上传者 GitHub 账号先到先得) const idx = await loadIndex(); const entries = idx.get(c.Package) || []; let verState = "", verOk = true; if (!entries.length) { - verState = `新包名,首次提交后归属于你${me ? `(@${me.login})` : ""}`; + verState = `${me ? t("preview.newPkg", { login: me.login }) : t("preview.newPkgAnon")}`; } else { // 前端只能读到线上索引里的 Maintainer,尽力从 noreply 地址反推 owner; // 服务端会按记录的 uploaded_by 权威复核。 @@ -245,12 +315,12 @@ async function renderPreview() { const latest = entries.map((e) => e.Version).sort(compareDebVersions).pop(); if (ownerLogin && !owned) { verOk = false; - verState = `包名已被 @${ownerLogin} 占用,只有其本人或管理员可以更新`; + verState = `${t("preview.ownedBy", { login: ownerLogin })}`; } else if (compareDebVersions(c.Version, latest) <= 0) { verOk = false; - verState = `版本 ${c.Version} 不高于线上已发布的 ${latest},请提升版本号`; + verState = `${t("preview.versionTooLow", { version: c.Version, latest })}`; } else { - verState = `已发布 ${latest} → 本次更新为 ${c.Version}`; + verState = `${t("preview.versionUpdate", { latest, version: c.Version })}`; } } $("p-verstate").innerHTML = verState; @@ -266,7 +336,7 @@ async function renderPreview() { } // 文件清单 + 脚本 - $("p-filecount").textContent = parsed.files.length; + $("p-files-summary").textContent = t("preview.filesSummary", { count: parsed.files.length }); $("p-files").textContent = parsed.files .filter((f) => f.type !== "dir") .map((f) => `${(f.mode & 0o7777).toString(8).padStart(4, "0")} ${String(f.size).padStart(9)} ${f.path}${f.linkname ? " -> " + f.linkname : ""}`) @@ -281,7 +351,7 @@ async function renderPreview() { const blocked = parsed.verdict === "danger" || !me || !verOk; $("submit-btn").disabled = blocked; - $("submit-btn").textContent = blocked ? "存在阻断性问题,无法提交" : "提交到 AppStore"; + $("submit-btn").textContent = blocked ? t("preview.blocked") : t("preview.submit"); $("preview-box").classList.remove("hidden"); } @@ -308,7 +378,7 @@ function fileToImage(f) { const img = new Image(); const url = URL.createObjectURL(f); img.onload = () => { URL.revokeObjectURL(url); resolve(img); }; - img.onerror = () => { URL.revokeObjectURL(url); reject(new Error("图片无法读取")); }; + img.onerror = () => { URL.revokeObjectURL(url); reject(new Error(t("form.imageUnreadable"))); }; img.src = url; }); } @@ -330,12 +400,12 @@ function renderShots() { storeShots.forEach((sh, i) => { const div = document.createElement("div"); div.className = "shot"; - div.innerHTML = `截图 ${i + 1}`; + div.innerHTML = `${t(`; div.querySelector("img").src = sh.url; box.appendChild(div); }); $("s-shot-btn").disabled = storeShots.length >= 6; - $("s-shot-btn").textContent = storeShots.length >= 6 ? "已达 6 张上限" : "+ 添加截图"; + $("s-shot-btn").textContent = storeShots.length >= 6 ? t("form.shotLimit") : t("form.addShot"); } $("s-shots").addEventListener("click", (e) => { @@ -355,7 +425,7 @@ $("s-shot-file").addEventListener("change", async (e) => { const blob = await coverToPng(await fileToImage(f), 320, 170); storeShots.push({ blob, url: URL.createObjectURL(blob) }); renderShots(); - } catch (err) { say("err", `截图处理失败:${err.message}`); } + } catch (err) { say("err", t("form.shotProcessFailed", { msg: err.message })); } }); $("s-icon-btn").addEventListener("click", () => $("s-icon").click()); @@ -374,7 +444,7 @@ $("s-icon").addEventListener("change", async (e) => { $("s-icon-preview").src = URL.createObjectURL(storeIcon); $("s-icon-preview").classList.remove("hidden"); $("s-icon-clear").classList.remove("hidden"); - } catch (err) { say("err", `图标处理失败:${err.message}`); } + } catch (err) { say("err", t("form.iconProcessFailed", { msg: err.message })); } }); /* --------------------------------- submit -------------------------------- */ @@ -382,7 +452,7 @@ $("s-icon").addEventListener("change", async (e) => { $("submit-btn").addEventListener("click", async () => { if (!file || !parsed) return; $("submit-btn").disabled = true; - say("", "上传中…"); + say("", t("upload.uploading")); const body = new FormData(); body.append("deb", file); @@ -405,10 +475,13 @@ $("submit-btn").addEventListener("click", async () => { const r = await fetch("/api/submit", { method: "POST", body }); const data = await r.json(); if (!r.ok) throw new Error(data.detail || data.error || `HTTP ${r.status}`); - say("ok", - `✓ ${data.message}\n审核进度:${data.actions_url}\n发布 PR:${data.track_url}`); + say("ok", t("upload.submitOk", { + message: data.message, + actions: data.actions_url, + track: data.track_url, + })); } catch (err) { - say("err", `提交失败:${err.message}`); + say("err", t("upload.submitFailed", { msg: err.message })); $("submit-btn").disabled = false; } }); @@ -420,7 +493,7 @@ async function renderMine() { const rows = $("mine-rows"); // Show a spinner while the (possibly slow) APT index fetch is in flight, so // an empty table never looks like a broken page. - rows.innerHTML = `
正在读取你的软件包…
`; + rows.innerHTML = `
${t("mine.loading")}
`; const idx = await loadIndex(); const mine = []; for (const [name, entries] of idx) { @@ -432,7 +505,7 @@ async function renderMine() { } } if (!mine.length) { - rows.innerHTML = `没有找到属于你的已发布软件包`; + rows.innerHTML = `${t("mine.empty")}`; return; } rows.innerHTML = ""; @@ -444,16 +517,16 @@ async function renderMine() { if (dl) { const a = document.createElement("a"); a.className = "dl-btn"; - a.textContent = "下载 .deb"; + a.textContent = t("mine.download"); a.href = dl; // Hint the browser to save instead of navigate; the canonical // pkg_version_arch.deb name also survives cross-origin redirects. a.download = `${p.name}_${p.Version}_${p.Architecture || "arm64"}.deb`; - a.title = `${p.name} ${p.Version}(${p.Size ? (p.Size / 1048576).toFixed(1) + " MB" : "大小未知"})`; + a.title = `${p.name} ${p.Version}(${p.Size ? (p.Size / 1048576).toFixed(1) + " MB" : t("mine.sizeUnknown")})`; actions.appendChild(a); } const btn = document.createElement("button"); - btn.textContent = "下架"; + btn.textContent = t("mine.unpublish"); btn.addEventListener("click", () => unpublish(p, btn)); actions.appendChild(btn); rows.appendChild(tr); @@ -471,7 +544,7 @@ function debDownloadUrl(entry) { } async function unpublish(p, btn) { - if (!confirm(`确认下架 ${p.name} ${p.Version}?将生成移除 PR。`)) return; + if (!confirm(t("mine.confirmUnpublish", { name: p.name, version: p.Version }))) return; btn.disabled = true; try { const r = await fetch("/api/unpublish", { @@ -481,9 +554,9 @@ async function unpublish(p, btn) { }); const data = await r.json(); if (!r.ok) throw new Error(data.detail || data.error); - btn.textContent = "已提交"; + btn.textContent = t("mine.submitted"); } catch (err) { - alert(`下架失败:${err.message}`); + alert(t("mine.unpublishFailed", { msg: err.message })); btn.disabled = false; } } diff --git a/site/i18n/en.json b/site/i18n/en.json new file mode 100644 index 0000000..408a00c --- /dev/null +++ b/site/i18n/en.json @@ -0,0 +1,77 @@ +{ + "meta": { "title": "Developer Center · CardputerZero AppStore" }, + "language": { "label": "Language" }, + "header": { "title": "CardputerZero Developer Center", "logout": "Sign out", "admin": " (admin)" }, + "boot": { "loading": "Loading…" }, + "login": { + "intro": "Sign in with GitHub to upload and manage AppStore packages.
Package names are first-come, first-served: whoever first uploads a name owns it under their GitHub account,
and only the original uploader (or an admin) can update or unpublish that name afterwards.
", + "button": "Sign in with GitHub" + }, + "tabs": { "upload": "Upload package", "mine": "My packages" }, + "upload": { + "dropHtml": "Drag a .deb here, or click to choose a file
Parsed locally as soon as you pick it — nothing is uploaded yet", + "chosen": "Selected {name} ({size} MB) — click to replace", + "pickDeb": "Please choose a .deb file", + "localParsing": "Parsing locally…", + "parseFailed": "Parse failed: {msg}", + "uploading": "Uploading…", + "submitOk": "✓ {message}\nReview progress: {actions}\nPublish PR: {track}", + "submitFailed": "Submission failed: {msg}" + }, + "preview": { + "noIcon": "No icon", + "version": "Version", + "installSize": "Installed size", + "maintainerMissing": "(missing)", + "reportTitle": "Preliminary check report", + "filesSummary": "File list ({count} files)", + "scripts": "Maintainer script contents", + "titlePlaceholder": "Name shown in the AppStore", + "uploaderAs": "Will be recorded as uploaded by @{login}", + "needLogin": "Please sign in with GitHub before submitting", + "newPkg": "New package name — after your first submission it belongs to you (@{login})", + "newPkgAnon": "New package name — after your first submission it belongs to you", + "ownedBy": "This package name is owned by @{login}; only they or an admin can update it", + "versionTooLow": "Version {version} is not higher than the published {latest}; please bump the version", + "versionUpdate": "Published {latest} → this update is {version}", + "submit": "Submit to AppStore", + "blocked": "Blocking issues found — cannot submit" + }, + "form": { + "sourceRepoLabel": "Source repository (optional, public repo; a store section in app-builder.json can auto-fill screenshots and other store info)", + "storeLegend": "Store info (optional; overrides info auto-filled from the source repo / deb)", + "appName": "App name", + "summary": "One-line summary", + "summaryPlaceholder": "Up to 80 characters", + "description": "Full description", + "descPlaceholder": "Multiple lines supported", + "categories": "Categories (comma-separated, up to 6)", + "iconLabel": "Icon (optional, square PNG; leave empty to use the icon inside the deb)", + "pickIcon": "Choose icon", + "removeIcon": "Remove", + "screenshotsLabel": "Screenshots (320×170, auto-cropped and resized; up to 6)", + "addShot": "+ Add screenshot", + "shotLimit": "Reached the 6-image limit", + "shotAlt": "Screenshot {n}", + "shotProcessFailed": "Screenshot processing failed: {msg}", + "iconProcessFailed": "Icon processing failed: {msg}", + "imageUnreadable": "Image could not be read" + }, + "mine": { + "colPackage": "Package", + "colVersion": "Version", + "colMaintainer": "Maintainer", + "loading": "Loading your packages…", + "empty": "No published packages found that belong to you", + "download": "Download .deb", + "unpublish": "Unpublish", + "submitted": "Submitted", + "sizeUnknown": "size unknown", + "note": "The list comes from the live APT index and only shows packages that belong to you. You can download published .deb files directly; unpublishing opens a removal PR.", + "confirmUnpublish": "Unpublish {name} {version}? This opens a removal PR.", + "unpublishFailed": "Unpublish failed: {msg}" + }, + "decompress": { + "loadFailed": "Could not load the {what} decompression module (used only for local preview). Please refresh and retry; you can still submit — the server completes validation." + } +} diff --git a/site/i18n/ja.json b/site/i18n/ja.json new file mode 100644 index 0000000..fee851d --- /dev/null +++ b/site/i18n/ja.json @@ -0,0 +1,77 @@ +{ + "meta": { "title": "開発者センター · CardputerZero AppStore" }, + "language": { "label": "言語" }, + "header": { "title": "CardputerZero 開発者センター", "logout": "ログアウト", "admin": "(管理者)" }, + "boot": { "loading": "読み込み中…" }, + "login": { + "intro": "AppStore パッケージのアップロード・管理には GitHub ログインが必要です。
パッケージ名は先着順です。最初にアップロードした人の GitHub アカウントに帰属し、
以降は元のアップロード者(または管理者)だけが更新・公開停止できます。
", + "button": "GitHub でログイン" + }, + "tabs": { "upload": "パッケージをアップロード", "mine": "マイパッケージ" }, + "upload": { + "dropHtml": ".deb をここにドラッグ、またはクリックしてファイルを選択
選択するとすぐにローカルで解析します(まだアップロードされません)", + "chosen": "選択済み {name}({size} MB)— クリックで変更", + "pickDeb": ".deb ファイルを選択してください", + "localParsing": "ローカルで解析中…", + "parseFailed": "解析に失敗しました:{msg}", + "uploading": "アップロード中…", + "submitOk": "✓ {message}\n審査の進捗:{actions}\n公開 PR:{track}", + "submitFailed": "送信に失敗しました:{msg}" + }, + "preview": { + "noIcon": "アイコンなし", + "version": "バージョン", + "installSize": "インストール容量", + "maintainerMissing": "(未設定)", + "reportTitle": "事前チェック結果", + "filesSummary": "ファイル一覧({count} 個)", + "scripts": "maintainer スクリプトの内容", + "titlePlaceholder": "AppStore に表示される名前", + "uploaderAs": "@{login} としてアップロード者に記録されます", + "needLogin": "送信前に GitHub にログインしてください", + "newPkg": "新しいパッケージ名です。最初の送信であなた(@{login})に帰属します", + "newPkgAnon": "新しいパッケージ名です。最初の送信であなたに帰属します", + "ownedBy": "このパッケージ名は @{login} が所有しています。本人または管理者のみ更新できます", + "versionTooLow": "バージョン {version} は公開済みの {latest} 以下です。バージョンを上げてください", + "versionUpdate": "公開済み {latest} → 今回の更新は {version}", + "submit": "AppStore に送信", + "blocked": "ブロッキングな問題があり送信できません" + }, + "form": { + "sourceRepoLabel": "ソースリポジトリ(任意・公開リポジトリ。app-builder.json の store セクションからスクリーンショット等のストア情報を自動取得できます)", + "storeLegend": "ストア情報(任意。入力するとソースリポジトリ/deb からの自動取得情報を上書きします)", + "appName": "アプリ名", + "summary": "ひとことの概要", + "summaryPlaceholder": "80 文字以内", + "description": "詳細な説明", + "descPlaceholder": "複数行に対応", + "categories": "カテゴリ(カンマ区切り・最大 6 個)", + "iconLabel": "アイコン(任意・正方形 PNG。未指定なら deb 内のアイコンを使用)", + "pickIcon": "アイコンを選択", + "removeIcon": "削除", + "screenshotsLabel": "スクリーンショット(320×170、選択後に自動でトリミング・リサイズ・最大 6 枚)", + "addShot": "+ スクリーンショットを追加", + "shotLimit": "6 枚の上限に達しました", + "shotAlt": "スクリーンショット {n}", + "shotProcessFailed": "スクリーンショットの処理に失敗しました:{msg}", + "iconProcessFailed": "アイコンの処理に失敗しました:{msg}", + "imageUnreadable": "画像を読み込めませんでした" + }, + "mine": { + "colPackage": "パッケージ", + "colVersion": "バージョン", + "colMaintainer": "Maintainer", + "loading": "マイパッケージを読み込み中…", + "empty": "あなたに帰属する公開済みパッケージは見つかりませんでした", + "download": ".deb をダウンロード", + "unpublish": "公開停止", + "submitted": "送信済み", + "sizeUnknown": "サイズ不明", + "note": "一覧はオンラインの APT インデックスから取得し、あなたに帰属するパッケージのみ表示します。公開済みの .deb は直接ダウンロードでき、公開停止は削除 PR を作成します。", + "confirmUnpublish": "{name} {version} を公開停止しますか?削除 PR を作成します。", + "unpublishFailed": "公開停止に失敗しました:{msg}" + }, + "decompress": { + "loadFailed": "{what} の解凍モジュールを読み込めませんでした(ローカルプレビュー用)。更新して再試行してください。そのまま送信もでき、サーバー側で検証します。" + } +} diff --git a/site/i18n/zh-CN.json b/site/i18n/zh-CN.json new file mode 100644 index 0000000..1b3861e --- /dev/null +++ b/site/i18n/zh-CN.json @@ -0,0 +1,77 @@ +{ + "meta": { "title": "开发者中心 · CardputerZero AppStore" }, + "language": { "label": "语言" }, + "header": { "title": "CardputerZero 开发者中心", "logout": "退出", "admin": "(管理员)" }, + "boot": { "loading": "加载中…" }, + "login": { + "intro": "上传 / 管理 AppStore 软件包需要 GitHub 登录。
包名先到先得:谁先上传某个包名,就归属于其 GitHub 账号,
之后同名包只有原上传者(或管理员)可以更新和下架。
", + "button": "使用 GitHub 登录" + }, + "tabs": { "upload": "上传软件包", "mine": "我的软件包" }, + "upload": { + "dropHtml": "拖拽 .deb 到这里,或点击选择文件
选择后立即在本地解析,不会上传", + "chosen": "已选择 {name}({size} MB)— 点击可更换", + "pickDeb": "请选择 .deb 文件", + "localParsing": "本地解析中…", + "parseFailed": "解析失败:{msg}", + "uploading": "上传中…", + "submitOk": "✓ {message}\n审核进度:{actions}\n发布 PR:{track}", + "submitFailed": "提交失败:{msg}" + }, + "preview": { + "noIcon": "无图标", + "version": "版本", + "installSize": "安装体积", + "maintainerMissing": "(缺失)", + "reportTitle": "初步检查报告", + "filesSummary": "文件清单({count} 个文件)", + "scripts": "maintainer 脚本内容", + "titlePlaceholder": "显示在 AppStore 里的名字", + "uploaderAs": "将以 @{login} 的身份记录为上传者", + "needLogin": "请先登录 GitHub 再提交", + "newPkg": "新包名,首次提交后归属于你(@{login})", + "newPkgAnon": "新包名,首次提交后归属于你", + "ownedBy": "包名已被 @{login} 占用,只有其本人或管理员可以更新", + "versionTooLow": "版本 {version} 不高于线上已发布的 {latest},请提升版本号", + "versionUpdate": "已发布 {latest} → 本次更新为 {version}", + "submit": "提交到 AppStore", + "blocked": "存在阻断性问题,无法提交" + }, + "form": { + "sourceRepoLabel": "源码仓库(可选,公开仓库;含 app-builder.json 的 store 段可自动带入截图等商店信息)", + "storeLegend": "商店信息(可选,填了会覆盖源码仓库/deb 自动带入的信息)", + "appName": "应用名称", + "summary": "一句话简介", + "summaryPlaceholder": "80 字以内", + "description": "详细描述", + "descPlaceholder": "支持多行", + "categories": "分类(逗号分隔,最多 6 个)", + "iconLabel": "图标(可选,正方形 PNG;留空则用 deb 内图标)", + "pickIcon": "选择图标", + "removeIcon": "移除", + "screenshotsLabel": "应用截图(320×170,选图后自动裁剪缩放;最多 6 张)", + "addShot": "+ 添加截图", + "shotLimit": "已达 6 张上限", + "shotAlt": "截图 {n}", + "shotProcessFailed": "截图处理失败:{msg}", + "iconProcessFailed": "图标处理失败:{msg}", + "imageUnreadable": "图片无法读取" + }, + "mine": { + "colPackage": "包名", + "colVersion": "版本", + "colMaintainer": "Maintainer", + "loading": "正在读取你的软件包…", + "empty": "没有找到属于你的已发布软件包", + "download": "下载 .deb", + "unpublish": "下架", + "submitted": "已提交", + "sizeUnknown": "大小未知", + "note": "列表来自线上 APT 索引,只显示归属于你的包。可直接下载已发布的 .deb;下架会生成移除 PR。", + "confirmUnpublish": "确认下架 {name} {version}?将生成移除 PR。", + "unpublishFailed": "下架失败:{msg}" + }, + "decompress": { + "loadFailed": "无法加载 {what} 解压组件(仅用于本地预览),请刷新重试;仍可直接提交,服务器会完成校验" + } +} diff --git a/site/index.html b/site/index.html index dcdb0b6..1893dad 100644 --- a/site/index.html +++ b/site/index.html @@ -20,8 +20,17 @@ padding: 14px 22px; border-bottom: 1px solid var(--line); } header h1 { font-size: 15px; margin: 0; color: var(--accent); } + header .header-actions { display: flex; align-items: center; gap: 14px; } header .who { font-size: 13px; color: var(--dim); } header .who a { color: var(--dim); } + select.locale-select { + background: var(--panel); color: var(--text); border: 1px solid var(--line); + border-radius: 6px; padding: 5px 8px; font: inherit; font-size: 13px; cursor: pointer; + } + .visually-hidden { + position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; + overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; + } main { max-width: 860px; margin: 0 auto; padding: 24px 16px 60px; } .tabs { display: flex; gap: 8px; margin-bottom: 18px; } .tabs button { @@ -111,46 +120,54 @@
-

CardputerZero 开发者中心

- +

CardputerZero 开发者中心

+
+ + + +
-
加载中…
+
加载中…