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 = `
`;
+ div.innerHTML = `
`;
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 开发者中心
+
- 上传 / 管理 AppStore 软件包需要 GitHub 登录。
+
上传 / 管理 AppStore 软件包需要 GitHub 登录。
包名先到先得:谁先上传某个包名,就归属于其 GitHub 账号,
之后同名包只有原上传者(或管理员)可以更新和下架。
-
+
-
-
+
+
- 拖拽 .deb 到这里,或点击选择文件
+ 拖拽 .deb 到这里,或点击选择文件
选择后立即在本地解析,不会上传
![app icon]()
-
无图标
+
无图标
-
版本 · · 安装体积
+
版本 · · 安装体积
Maintainer:
@@ -158,46 +175,46 @@
CardputerZero 开发者中心
-
文件清单( 个文件)
-
maintainer 脚本内容
+
+
maintainer 脚本内容
-
+
-
+
@@ -205,10 +222,10 @@
CardputerZero 开发者中心
- | 包名 | 版本 | Maintainer | |
- 正在读取你的软件包… |
+ | 包名 | 版本 | Maintainer | |
+ 正在读取你的软件包… |
-
列表来自线上 APT 索引,只显示归属于你的包。可直接下载已发布的 .deb;下架会生成移除 PR。
+
列表来自线上 APT 索引,只显示归属于你的包。可直接下载已发布的 .deb;下架会生成移除 PR。