add: 添加 i18n 翻译
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user