add: 添加 i18n 翻译
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
|
||||
import { post } from "../rc.js";
|
||||
import { getState, setState, toast } from "../state.js";
|
||||
import { t } from "../i18n.js";
|
||||
|
||||
// --- Route entrypoints ---
|
||||
|
||||
@@ -23,9 +24,9 @@ export async function renderConfigureNew({ provider = "" }) {
|
||||
if (!info) {
|
||||
document.getElementById("app").innerHTML = `
|
||||
<div class="empty">
|
||||
<h3>Unknown backend</h3>
|
||||
<p>No provider named <code>${escapeHtml(provider)}</code>.</p>
|
||||
<p><a href="#/configure/new">← Back to provider picker</a></p>
|
||||
<h3>${t("error.unknown_remote")}</h3>
|
||||
<p>${t("error.no_such_provider", provider)}</p>
|
||||
<p><a href="#/configure/new">← ${t("configure.new_title")}</a></p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
@@ -38,23 +39,22 @@ export async function renderConfigureEdit({ remote }) {
|
||||
return;
|
||||
}
|
||||
const app = document.getElementById("app");
|
||||
app.innerHTML = `<div class="empty"><p>Loading remote <code>${escapeHtml(remote)}</code>…</p></div>`;
|
||||
app.innerHTML = `<div class="empty"><p>${t("loading.title")}…</p></div>`;
|
||||
|
||||
// Find the remote's type from /config/dump
|
||||
let typeName;
|
||||
try {
|
||||
const dump = await post("config/dump");
|
||||
typeName = dump && dump[remote] && dump[remote].type;
|
||||
} catch (e) {
|
||||
toast(`Couldn't read remote: ${e.message}`, "error");
|
||||
toast(`${t("error.couldnt_load")}: ${e.message}`, "error");
|
||||
return;
|
||||
}
|
||||
if (!typeName) {
|
||||
app.innerHTML = `
|
||||
<div class="empty">
|
||||
<h3>Remote not found</h3>
|
||||
<p>No remote named <code>${escapeHtml(remote)}</code>.</p>
|
||||
<p><a href="#/remotes">← Back to remotes</a></p>
|
||||
<h3>${t("error.unknown_remote")}</h3>
|
||||
<p>${t("error.no_such_remote", remote)}</p>
|
||||
<p><a href="#/remotes">← ${t("nav.remotes")}</a></p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export async function renderConfigureEdit({ remote }) {
|
||||
const providers = await ensureProviders();
|
||||
const info = providers.find((p) => p.Name === typeName);
|
||||
if (!info) {
|
||||
toast(`Backend ${typeName} not found in registry`, "error");
|
||||
toast(`${t("error.no_such_provider", typeName)}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export async function renderConfigureEdit({ remote }) {
|
||||
try {
|
||||
currentValues = await post("config/get", { name: remote });
|
||||
} catch (e) {
|
||||
toast(`Couldn't read config: ${e.message}`, "error");
|
||||
toast(`${t("error.couldnt_load")}: ${e.message}`, "error");
|
||||
}
|
||||
|
||||
return renderForm({
|
||||
@@ -88,16 +88,16 @@ async function renderProviderPicker() {
|
||||
app.innerHTML = `
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>New remote</h2>
|
||||
<p class="subtitle">Pick a storage backend to configure.</p>
|
||||
<h2>${t("configure.new_title")}</h2>
|
||||
<p class="subtitle">${t("configure.new_subtitle")}</p>
|
||||
</div>
|
||||
<a class="btn btn-secondary btn-sm" href="#/remotes">Cancel</a>
|
||||
<a class="btn btn-secondary btn-sm" href="#/remotes">${t("configure.cancel")}</a>
|
||||
</div>
|
||||
<div class="provider-search">
|
||||
<input id="provider-filter" class="input" type="search" placeholder="Filter backends (e.g. s3, sftp, local)…" autocomplete="off">
|
||||
<input id="provider-filter" class="input" type="search" placeholder="${t("configure.filter_placeholder")}" autocomplete="off">
|
||||
</div>
|
||||
<div id="provider-grid" class="connector-grid">
|
||||
<p class="empty" style="grid-column:1/-1">Loading backends…</p>
|
||||
<p class="empty" style="grid-column:1/-1">${t("loading.title")}…</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -108,18 +108,17 @@ async function renderProviderPicker() {
|
||||
try {
|
||||
providers = await ensureProviders();
|
||||
} catch (e) {
|
||||
grid.innerHTML = `<div class="empty" style="grid-column:1/-1"><h3>Couldn’t load backends</h3><p>${escapeHtml(e.message)}</p></div>`;
|
||||
grid.innerHTML = `<div class="empty" style="grid-column:1/-1"><h3>${t("error.couldnt_load_backends")}</h3><p>${escapeHtml(e.message)}</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip hidden + alias/all wrapper backends
|
||||
const visible = providers.filter(
|
||||
(p) => !p.Hide && p.Name !== "all" && p.Name !== "alias",
|
||||
);
|
||||
|
||||
function paint(list) {
|
||||
if (list.length === 0) {
|
||||
grid.innerHTML = `<p class="empty" style="grid-column:1/-1">No backends match.</p>`;
|
||||
grid.innerHTML = `<p class="empty" style="grid-column:1/-1">${t("configure.no_match")}</p>`;
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = list
|
||||
@@ -165,10 +164,10 @@ async function renderForm({ provider, mode, remoteName = "", currentValues = {}
|
||||
app.innerHTML = `
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>${isEdit ? "Edit" : "New"} ${escapeHtml(provider.Name)} remote</h2>
|
||||
<h2>${isEdit ? t("configure.edit_title", provider.Name) : t("configure.create_title", provider.Name)}</h2>
|
||||
<p class="subtitle">${escapeHtml(provider.Description || "")}</p>
|
||||
</div>
|
||||
<a class="btn btn-secondary btn-sm" href="#/remotes">Cancel</a>
|
||||
<a class="btn btn-secondary btn-sm" href="#/remotes">${t("configure.cancel")}</a>
|
||||
</div>
|
||||
|
||||
<form id="remote-form" class="card-outline" style="display:flex;flex-direction:column;gap:16px;max-width:760px">
|
||||
@@ -176,7 +175,7 @@ async function renderForm({ provider, mode, remoteName = "", currentValues = {}
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field field-full">
|
||||
<label>Remote name <span class="field-required">*</span></label>
|
||||
<label>${t("configure.name")} <span class="field-required">*</span></label>
|
||||
<input
|
||||
class="input"
|
||||
name="_remote_name"
|
||||
@@ -187,20 +186,20 @@ async function renderForm({ provider, mode, remoteName = "", currentValues = {}
|
||||
>
|
||||
</div>
|
||||
|
||||
${required.length > 0 ? `<div class="form-section-title">Required</div>` : ""}
|
||||
${required.length > 0 ? `<div class="form-section-title">${t("configure.section.required")}</div>` : ""}
|
||||
${required.map((opt) => fieldHtml(opt, currentValues, isEdit)).join("")}
|
||||
|
||||
${basic.length > 0 ? `<div class="form-section-title">Options</div>` : ""}
|
||||
${basic.length > 0 ? `<div class="form-section-title">${t("configure.section.options")}</div>` : ""}
|
||||
${basic.map((opt) => fieldHtml(opt, currentValues, isEdit)).join("")}
|
||||
|
||||
${advanced.length > 0 ? `
|
||||
<div class="advanced-toggle-wrap">
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="advanced-toggle">
|
||||
Show advanced options (${advanced.length})
|
||||
${t("configure.show_advanced", advanced.length)}
|
||||
</button>
|
||||
</div>
|
||||
<div id="advanced-section" class="advanced-section hidden">
|
||||
<div class="form-section-title">Advanced</div>
|
||||
<div class="form-section-title">${t("configure.section.advanced")}</div>
|
||||
${advanced.map((opt) => fieldHtml(opt, currentValues, isEdit)).join("")}
|
||||
</div>
|
||||
` : ""}
|
||||
@@ -208,16 +207,16 @@ async function renderForm({ provider, mode, remoteName = "", currentValues = {}
|
||||
|
||||
<div class="toolbar">
|
||||
<button type="submit" class="btn btn-primary" ${requiresOAuth ? "disabled" : ""}>
|
||||
${isEdit ? "Save changes" : "Create remote"}
|
||||
${isEdit ? t("configure.save") : t("configure.create")}
|
||||
</button>
|
||||
<a class="btn btn-secondary" href="#/remotes">Cancel</a>
|
||||
<a class="btn btn-secondary" href="#/remotes">${t("configure.cancel")}</a>
|
||||
</div>
|
||||
|
||||
${isEdit ? `
|
||||
<div class="danger-zone">
|
||||
<h4>Delete this remote</h4>
|
||||
<p>Permanently remove <code>${escapeHtml(remoteName)}</code> from rclone.conf.</p>
|
||||
<button type="button" class="btn btn-danger" id="delete-btn">Delete remote</button>
|
||||
<h4>${t("configure.delete_section_title")}</h4>
|
||||
<p>${t("configure.delete_section_body", escapeHtml(remoteName))}</p>
|
||||
<button type="button" class="btn btn-danger" id="delete-btn">${t("configure.delete_btn")}</button>
|
||||
</div>
|
||||
` : ""}
|
||||
</form>
|
||||
@@ -235,27 +234,25 @@ async function renderForm({ provider, mode, remoteName = "", currentValues = {}
|
||||
advToggle.addEventListener("click", () => {
|
||||
const hidden = advSection.classList.toggle("hidden");
|
||||
advToggle.textContent = hidden
|
||||
? `Show advanced options (${advanced.length})`
|
||||
: `Hide advanced options (${advanced.length})`;
|
||||
? t("configure.show_advanced", advanced.length)
|
||||
: t("configure.hide_advanced", advanced.length);
|
||||
});
|
||||
}
|
||||
|
||||
// Delete button
|
||||
const deleteBtn = document.getElementById("delete-btn");
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener("click", async () => {
|
||||
if (!confirm(`Delete remote ${remoteName}? This cannot be undone.`)) return;
|
||||
if (!confirm(t("remotes.delete_confirm", remoteName))) return;
|
||||
try {
|
||||
await post("config/delete", { name: remoteName });
|
||||
toast(`Deleted ${remoteName}`, "success");
|
||||
toast(t("remotes.deleted", remoteName), "success");
|
||||
location.hash = "#/remotes";
|
||||
} catch (e) {
|
||||
toast(`Delete failed: ${e.message}`, "error");
|
||||
toast(`${t("remotes.delete_failed")}: ${e.message}`, "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Submit
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (requiresOAuth) return;
|
||||
@@ -275,10 +272,10 @@ async function renderForm({ provider, mode, remoteName = "", currentValues = {}
|
||||
|
||||
const endpoint = isEdit ? "config/update" : "config/create";
|
||||
await post(endpoint, body);
|
||||
toast(`${isEdit ? "Updated" : "Created"} ${name}`, "success");
|
||||
toast(isEdit ? t("configure.updated", name) : t("configure.created", name), "success");
|
||||
location.hash = "#/remotes";
|
||||
} catch (err) {
|
||||
toast(`Save failed: ${err.message}`, "error");
|
||||
toast(`${t("configure.save_failed")}: ${err.message}`, "error");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -289,11 +286,10 @@ function oauthBanner(providerName) {
|
||||
return `
|
||||
<div class="banner banner-warning">
|
||||
<div>
|
||||
<strong>${escapeHtml(providerName)}</strong> requires OAuth authorization,
|
||||
which this web GUI can’t complete inside a container. Please configure it
|
||||
from a terminal first:
|
||||
<strong>${t("configure.oauth_title")}</strong>
|
||||
${t("configure.oauth_body", escapeHtml(providerName))}
|
||||
<code>rclone config</code>
|
||||
Once the remote exists, you can edit its non-secret options here.
|
||||
${t("configure.oauth_hint")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -327,7 +323,7 @@ function inputHtmlFor(opt, value, isEdit) {
|
||||
// already obscured and resubmitting would double-obscure. Show empty
|
||||
// with a "(unchanged)" placeholder.
|
||||
if (opt.IsPassword && isEdit) {
|
||||
return `<input class="input" name="opt_${escapeHtml(opt.Name)}" type="password" autocomplete="off" placeholder="(unchanged)">`;
|
||||
return `<input class="input" name="opt_${escapeHtml(opt.Name)}" type="password" autocomplete="off" placeholder="${t("configure.password_unchanged")}">`;
|
||||
}
|
||||
|
||||
const name = `opt_${escapeHtml(opt.Name)}`;
|
||||
@@ -343,12 +339,11 @@ function inputHtmlFor(opt, value, isEdit) {
|
||||
}
|
||||
|
||||
if (opt.IsPassword) {
|
||||
return `<input class="input" name="${name}" type="password" autocomplete="off" placeholder="secret">`;
|
||||
return `<input class="input" name="${name}" type="password" autocomplete="off" placeholder="${t("configure.password_secret")}">`;
|
||||
}
|
||||
|
||||
// Examples → select with custom override
|
||||
if (opt.Examples && opt.Examples.length > 0) {
|
||||
const opts = ['<option value="">— pick —</option>']
|
||||
const opts = [`<option value="">${t("configure.example_pick")}</option>`]
|
||||
.concat(
|
||||
opt.Examples.map(
|
||||
(ex) =>
|
||||
@@ -359,9 +354,9 @@ function inputHtmlFor(opt, value, isEdit) {
|
||||
return `
|
||||
<select class="select" name="${name}" data-has-custom="1">
|
||||
${opts}
|
||||
<option value="__custom__"${currentValue && !opt.Examples.some((e) => e.Value === currentValue) ? " selected" : ""}>Custom…</option>
|
||||
<option value="__custom__"${currentValue && !opt.Examples.some((e) => e.Value === currentValue) ? " selected" : ""}>${t("configure.example_custom")}</option>
|
||||
</select>
|
||||
<input class="input" name="${name}__custom" type="text" value="${escapeAttr(currentValue)}" style="margin-top:8px;display:none" placeholder="custom value" autocomplete="off">
|
||||
<input class="input" name="${name}__custom" type="text" value="${escapeAttr(currentValue)}" style="margin-top:8px;display:none" placeholder="${t("configure.example_custom_placeholder")}" autocomplete="off">
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user