add: 添加 i18n 翻译

This commit is contained in:
ci
2026-06-19 13:05:35 +08:00
parent 5ebe562a62
commit 73dfe12c95
8 changed files with 654 additions and 189 deletions
+47 -51
View File
@@ -11,6 +11,7 @@ import {
formatSpeed,
formatDuration,
} from "../state.js";
import { t } from "../i18n.js";
let pollTimer = null;
@@ -19,13 +20,13 @@ export async function renderJobs() {
app.innerHTML = `
<div class="section-head">
<div>
<h2>Jobs</h2>
<p class="subtitle">Running and recently completed transfers.</p>
<h2>${t("jobs.title")}</h2>
<p class="subtitle">${t("jobs.subtitle")}</p>
</div>
<a class="btn btn-primary btn-sm" href="#/jobs/new">New Job</a>
<a class="btn btn-primary btn-sm" href="#/jobs/new">${t("jobs.new_btn")}</a>
</div>
<div id="jobs-card" class="card-outline">
<p class="empty">Loading jobs…</p>
<p class="empty">${t("loading.title")}…</p>
</div>
`;
@@ -36,13 +37,12 @@ export async function renderJobs() {
export async function renderNewJob() {
const app = document.getElementById("app");
// Pull remotes for the dropdowns
let remotes = [];
try {
const res = await post("config/listremotes");
remotes = (res && res.remotes) || [];
} catch (e) {
toast(`Couldn’t load remotes: ${e.message}`, "error");
toast(`${t("error.couldnt_load_remotes")}: ${e.message}`, "error");
}
const remoteOpts = remotes
@@ -52,49 +52,48 @@ export async function renderNewJob() {
app.innerHTML = `
<div class="section-head">
<div>
<h2>New Job</h2>
<p class="subtitle">Copy, sync, or move between remotes.</p>
<h2>${t("jobs.new_title")}</h2>
<p class="subtitle">${t("jobs.new_subtitle")}</p>
</div>
<a class="btn btn-secondary btn-sm" href="#/jobs">Cancel</a>
<a class="btn btn-secondary btn-sm" href="#/jobs">${t("jobs.cancel")}</a>
</div>
<form id="new-job-form" class="card-outline" style="display:grid;gap:16px;max-width:640px">
<div class="field">
<label>Action</label>
<label>${t("jobs.action")}</label>
<select name="action" class="select">
<option value="copy">copy (mirror src → dst, keep both)</option>
<option value="sync">sync (mirror src → dst, delete extras on dst)</option>
<option value="move">move (mirror src → dst, delete src after)</option>
<option value="copy">${t("jobs.action_copy")}</option>
<option value="sync">${t("jobs.action_sync")}</option>
<option value="move">${t("jobs.action_move")}</option>
</select>
</div>
<div class="form-grid">
<div class="field">
<label>Source remote</label>
<label>${t("jobs.source_remote")}</label>
<select name="srcRemote" class="select">
${remoteOpts || '<option value="">(no remotes)</option>'}
${remoteOpts || `<option value="">${t("jobs.no_remotes_option")}</option>`}
</select>
</div>
<div class="field">
<label>Source path (optional)</label>
<input class="input" name="srcPath" placeholder="folder/subfolder" autocomplete="off">
<label>${t("jobs.source_path")}</label>
<input class="input" name="srcPath" placeholder="${t("jobs.path_placeholder")}" autocomplete="off">
</div>
<div class="field">
<label>Destination remote</label>
<label>${t("jobs.dest_remote")}</label>
<select name="dstRemote" class="select">
${remoteOpts || '<option value="">(no remotes)</option>'}
${remoteOpts || `<option value="">${t("jobs.no_remotes_option")}</option>`}
</select>
</div>
<div class="field">
<label>Destination path (optional)</label>
<input class="input" name="dstPath" placeholder="folder/subfolder" autocomplete="off">
<label>${t("jobs.dest_path")}</label>
<input class="input" name="dstPath" placeholder="${t("jobs.path_placeholder")}" autocomplete="off">
</div>
</div>
<div class="toolbar">
<button type="submit" class="btn btn-primary">Start Job</button>
<button type="submit" class="btn btn-primary">${t("jobs.start")}</button>
</div>
</form>
`;
// Prefill first remote in both selectors for convenience
if (remotes.length > 0) {
app.querySelector('select[name="srcRemote"]').value = `${remotes[0]}:`;
app.querySelector('select[name="dstRemote"]').value = `${remotes[0]}:`;
@@ -109,7 +108,7 @@ export async function renderNewJob() {
const srcPath = form.elements.srcPath.value.trim().replace(/^\/+|\/+$/g, "");
const dstPath = form.elements.dstPath.value.trim().replace(/^\/+|\/+$/g, "");
if (!srcRemote || !dstRemote) {
toast("Pick source and destination remotes", "error");
toast(t("error.no_remotes_selected"), "error");
return;
}
const src = srcPath ? `${srcRemote}${srcPath}` : srcRemote;
@@ -122,41 +121,39 @@ export async function renderNewJob() {
if (jobid != null) {
rememberJob(jobid, { action, src, dst });
}
toast(`Started ${action} job #${jobid}`, "success");
toast(t("jobs.started", action, jobid), "success");
location.hash = "#/jobs";
} catch (e) {
toast(`Job start failed: ${e.message}`, "error");
toast(`${t("jobs.start_failed")}: ${e.message}`, "error");
}
});
}
async function refreshJobs() {
const card = document.getElementById("jobs-card");
if (!card) return; // user navigated away
if (!card) return;
let jobIds = [];
try {
const list = await post("job/list");
const running = (list && list.jobids) || [];
const finished = (list && list.finishedIds) || [];
// Show both running and recently-finished. Sort happens in render.
jobIds = [...new Set([...running, ...finished])];
} catch (e) {
card.innerHTML = `<div class="empty"><h3>Couldn’t load jobs</h3><p>${escapeHtml(e.message)}</p></div>`;
card.innerHTML = `<div class="empty"><h3>${t("error.couldnt_load_jobs")}</h3><p>${escapeHtml(e.message)}</p></div>`;
return;
}
if (jobIds.length === 0) {
card.innerHTML = `
<div class="empty">
<h3>No jobs yet</h3>
<p>Use <a href="#/jobs/new">New Job</a> to start a copy, sync, or move.</p>
<h3>${t("jobs.empty_title")}</h3>
<p>${t("jobs.empty_body").replace("New Job", `<a href="#/jobs/new">${t("jobs.new_btn")}</a>`)}</p>
</div>
`;
return;
}
// Fetch each job's status in parallel
const statuses = await Promise.all(
jobIds.map((id) =>
post("job/status", { jobid: id }).catch((e) => ({
@@ -174,7 +171,6 @@ async function refreshJobs() {
}
function renderJobTable(statuses) {
// Newest jobid first
statuses.sort((a, b) => (b.jobid ?? 0) - (a.jobid ?? 0));
const rows = statuses.map(renderJobRow).join("");
@@ -183,15 +179,15 @@ function renderJobTable(statuses) {
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Job</th>
<th>Status</th>
<th class="col-num">Progress</th>
<th class="col-num">Speed</th>
<th class="col-num">ETA</th>
<th class="col-num">Files</th>
<th class="col-num">Errors</th>
<th class="col-num">Action</th>
<th>${t("jobs.col.id")}</th>
<th>${t("jobs.col.job")}</th>
<th>${t("jobs.col.status")}</th>
<th class="col-num">${t("jobs.col.progress")}</th>
<th class="col-num">${t("jobs.col.speed")}</th>
<th class="col-num">${t("jobs.col.eta")}</th>
<th class="col-num">${t("jobs.col.files")}</th>
<th class="col-num">${t("jobs.col.errors")}</th>
<th class="col-num">${t("jobs.col.action")}</th>
</tr>
</thead>
<tbody>
@@ -210,13 +206,13 @@ function renderJobRow(s) {
let badge;
if (!finished) {
badge = `<span class="badge">running</span>`;
badge = `<span class="badge">${t("jobs.status.running")}</span>`;
} else if (errored || (!success && errored)) {
badge = `<span class="badge badge-error">failed</span>`;
badge = `<span class="badge badge-error">${t("jobs.status.failed")}</span>`;
} else if (success) {
badge = `<span class="badge badge-success">done</span>`;
badge = `<span class="badge badge-success">${t("jobs.status.done")}</span>`;
} else {
badge = `<span class="badge badge-warning">finished</span>`;
badge = `<span class="badge badge-warning">${t("jobs.status.finished")}</span>`;
}
const meta = getJobMeta(id);
@@ -230,7 +226,7 @@ function renderJobRow(s) {
<code>${escapeHtml(meta.dst)}</code>
</span>
</div>`
: `<span class="col-mono" style="color:var(--color-muted-soft)">— submitted via CLI —</span>`;
: `<span class="col-mono" style="color:var(--color-muted-soft)">${t("jobs.submitted_cli")}</span>`;
const pct = p && p.totalBytes > 0 ? Math.min(100, (p.bytes / p.totalBytes) * 100) : 0;
const progress = `
@@ -243,7 +239,7 @@ function renderJobRow(s) {
`;
const stopBtn = !finished
? `<button class="btn btn-danger btn-sm" data-stop="${id}">Stop</button>`
? `<button class="btn btn-danger btn-sm" data-stop="${id}">${t("jobs.stop")}</button>`
: "";
return `
@@ -262,13 +258,13 @@ function renderJobRow(s) {
}
async function onStop(jobid) {
if (!confirm(`Stop job #${jobid}?`)) return;
if (!confirm(t("jobs.stop_confirm", jobid))) return;
try {
await post("job/stop", { jobid });
toast(`Stopped job #${jobid}`, "success");
toast(t("jobs.stopped", jobid), "success");
await refreshJobs();
} catch (e) {
toast(`Stop failed: ${e.message}`, "error");
toast(`${t("jobs.stop_failed")}: ${e.message}`, "error");
}
}