translate 0.1.2 → 0.1.3: real icon, bigger text, zoom, auto-translate on language change
Auto-translate: changing either language only called saveUi(), so the
pane below kept showing the previous language's result with nothing to
say it was stale — you had to notice and press Translate. Both selects
now re-run the translation, guarded on empty input and on
source === target (which would only echo the input back).
Text size: the panes were 13.5px, small for reading a paragraph in a
language you don't know well, which is the entire job. Base is now
15px, driven by a --tsize custom property so both panes stay matched.
Zoom: −/+ either side of a percentage in the header, 70–220% in steps
of 10, clamped with the buttons disabling at each end. Click the
percentage to reset. Ctrl/Cmd with +, - or 0 does the same from either
pane. Persists per-machine alongside the language choice.
Icon: a globe with an A tile and a 文 tile, replacing the 🌐 emoji that
was indistinguishable from every other globe in the dock. Drawn against
the 16px and 18px rasterizations rather than at a comfortable size —
the first pass used a font glyph for 文 and it turned to grey mush at
16px, so the strokes are hand-drawn paths thick enough to survive. Also
drops the hardcoded icon in registerSidebarPanel, which would otherwise
shadow the manifest's mark.
Verified in a harness with a stubbed host API: language change fires
exactly one request, the two guards fire none, zoom clamps and persists,
and the layout holds at sidebar width.
This commit is contained in:
parent
a75707d0ed
commit
13a4dd692d
3 changed files with 87 additions and 13 deletions
|
|
@ -1,12 +1,15 @@
|
||||||
{
|
{
|
||||||
"id": "translate",
|
"id": "translate",
|
||||||
"name": "Translate",
|
"name": "Translate",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"description": "Right-click a selection to translate it. Sidebar panel with LibreTranslate or Google as the backend.",
|
"description": "Right-click a selection to translate it. Sidebar panel with LibreTranslate or Google as the backend.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
"icon": "🌐",
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij4gPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTEiIGZpbGw9IiMxZDRlZDgiLz4gPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEuMSIgb3BhY2l0eT0iLjQ1Ij48ZWxsaXBzZSBjeD0iMTIiIGN5PSIxMiIgcng9IjQuNiIgcnk9IjExIi8+PC9nPiA8cmVjdCB4PSIwLjYiIHk9IjEuOCIgd2lkdGg9IjExLjYiIGhlaWdodD0iMTEuNiIgcng9IjIuNiIgZmlsbD0iI2ZmZiIvPiA8dGV4dCB4PSI2LjQiIHk9IjExLjEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJzeXN0ZW0tdWksU2Vnb2UgVUksc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iIzFkNGVkOCI+QTwvdGV4dD4gPHJlY3QgeD0iMTEuOCIgeT0iMTAuNiIgd2lkdGg9IjExLjYiIGhlaWdodD0iMTEuNiIgcng9IjIuNiIgZmlsbD0iIzBiMTIyMCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4gPGcgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEuOSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBmaWxsPSJub25lIj4gPHBhdGggZD0iTTE3LjYgMTIuNXYxIi8+IDxwYXRoIGQ9Ik0xNC40IDE0LjdoNi40Ii8+IDxwYXRoIGQ9Ik0xOC4zIDE1LjYgMTQuNyAyMCIvPiA8cGF0aCBkPSJNMTYuOSAxNS42IDIwLjUgMjAiLz4gPC9nPiA8L3N2Zz4=",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"capabilities": ["sidebar-panel", "context-menu-item"],
|
"capabilities": [
|
||||||
|
"sidebar-panel",
|
||||||
|
"context-menu-item"
|
||||||
|
],
|
||||||
"context-menu-items": [
|
"context-menu-items": [
|
||||||
{
|
{
|
||||||
"id": "translate-selection",
|
"id": "translate-selection",
|
||||||
|
|
|
||||||
|
|
@ -100,10 +100,11 @@ async function doTranslate(payload) {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
activate(api) {
|
activate(api) {
|
||||||
|
// No `icon` here on purpose — registerSidebarPanel defaults to
|
||||||
|
// manifest.icon, and passing one would shadow the data-URI mark.
|
||||||
api.registerSidebarPanel({
|
api.registerSidebarPanel({
|
||||||
id: "main",
|
id: "main",
|
||||||
title: "Translate",
|
title: "Translate",
|
||||||
icon: "🌐",
|
|
||||||
page: "panel.html",
|
page: "panel.html",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,23 @@
|
||||||
header .m { color: var(--dim); font-size: 11.5px; }
|
header .m { color: var(--dim); font-size: 11.5px; }
|
||||||
header .m.err { color: #ff9081; }
|
header .m.err { color: #ff9081; }
|
||||||
header .m.ok { color: var(--acid); }
|
header .m.ok { color: var(--acid); }
|
||||||
|
header .t .em { display: inline-flex; }
|
||||||
|
header .t .em svg { display: block; width: 17px; height: 17px; }
|
||||||
|
.zoom { display: flex; align-items: center; gap: 2px; margin-left: 10px; }
|
||||||
|
.zoom button {
|
||||||
|
width: 20px; height: 20px; padding: 0; line-height: 1;
|
||||||
|
display: grid; place-items: center;
|
||||||
|
background: var(--btn); color: var(--mut);
|
||||||
|
border: 1px solid var(--line); border-radius: 5px;
|
||||||
|
cursor: pointer; font: inherit; font-size: 13px;
|
||||||
|
}
|
||||||
|
.zoom button:hover:not(:disabled) { background: var(--btn-h); color: var(--ink); }
|
||||||
|
.zoom button:disabled { opacity: .35; cursor: default; }
|
||||||
|
.zoom .pct {
|
||||||
|
min-width: 34px; text-align: center; color: var(--dim);
|
||||||
|
font-size: 10.5px; font-variant-numeric: tabular-nums; cursor: pointer;
|
||||||
|
}
|
||||||
|
.zoom .pct:hover { color: var(--ink); }
|
||||||
.langs { display: flex; gap: 6px; align-items: center;
|
.langs { display: flex; gap: 6px; align-items: center;
|
||||||
padding: 8px 12px; border-bottom: 1px solid var(--line); background: var(--panel2); }
|
padding: 8px 12px; border-bottom: 1px solid var(--line); background: var(--panel2); }
|
||||||
.langs select { flex: 1; min-width: 0; padding: 6px 8px; font: inherit;
|
.langs select { flex: 1; min-width: 0; padding: 6px 8px; font: inherit;
|
||||||
|
|
@ -43,10 +60,14 @@
|
||||||
.zone label .action { color: var(--mut); font-size: 10.5px; cursor: pointer;
|
.zone label .action { color: var(--mut); font-size: 10.5px; cursor: pointer;
|
||||||
letter-spacing: normal; text-transform: none; }
|
letter-spacing: normal; text-transform: none; }
|
||||||
.zone label .action:hover { color: var(--acid); }
|
.zone label .action:hover { color: var(--acid); }
|
||||||
|
/* --tsize drives both panes so zoom keeps them matched. 15px is the
|
||||||
|
default: the old 13.5px was small for reading a paragraph of a language
|
||||||
|
you don't know well, which is the whole job here. */
|
||||||
textarea, .output {
|
textarea, .output {
|
||||||
flex: 1; padding: 8px 14px 14px; border: none; outline: none; resize: none;
|
flex: 1; padding: 8px 14px 14px; border: none; outline: none; resize: none;
|
||||||
background: transparent; color: var(--ink);
|
background: transparent; color: var(--ink);
|
||||||
font: 13.5px/1.55 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||||
|
font-size: var(--tsize, 15px); line-height: 1.55;
|
||||||
overflow-y: auto; white-space: pre-wrap; word-wrap: break-word;
|
overflow-y: auto; white-space: pre-wrap; word-wrap: break-word;
|
||||||
}
|
}
|
||||||
textarea::placeholder { color: var(--dim); }
|
textarea::placeholder { color: var(--dim); }
|
||||||
|
|
@ -79,7 +100,15 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="t"><span class="em">🌐</span> <span>Translate</span></div>
|
<div class="t">
|
||||||
|
<span class="em" aria-hidden="true"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="11" fill="#1d4ed8"/><ellipse cx="12" cy="12" rx="4.6" ry="11" fill="none" stroke="#fff" stroke-width="1.1" opacity=".45"/><rect x="0.6" y="1.8" width="11.6" height="11.6" rx="2.6" fill="#fff"/><text x="6.4" y="11.1" text-anchor="middle" font-family="system-ui,Segoe UI,sans-serif" font-size="10" font-weight="800" fill="#1d4ed8">A</text><rect x="11.8" y="10.6" width="11.6" height="11.6" rx="2.6" fill="#0b1220" stroke="#fff" stroke-width="1"/><g stroke="#fff" stroke-width="1.9" stroke-linecap="round" fill="none"><path d="M17.6 12.5v1"/><path d="M14.4 14.7h6.4"/><path d="M18.3 15.6 14.7 20"/><path d="M16.9 15.6 20.5 20"/></g></svg></span>
|
||||||
|
<span>Translate</span>
|
||||||
|
<span class="zoom">
|
||||||
|
<button id="zoomOut" title="Smaller text (Ctrl+-)" aria-label="Smaller text">−</button>
|
||||||
|
<span class="pct" id="zoomPct" title="Reset to 100%" role="button" tabindex="0">100%</span>
|
||||||
|
<button id="zoomIn" title="Larger text (Ctrl++)" aria-label="Larger text">+</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="m" id="status">ready</div>
|
<div class="m" id="status">ready</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -176,7 +205,20 @@
|
||||||
// current working default so the panel isn't broken out of the box.
|
// current working default so the panel isn't broken out of the box.
|
||||||
const DEAD_LT_URLS = new Set(["https://translate.argosopentech.com"]);
|
const DEAD_LT_URLS = new Set(["https://translate.argosopentech.com"]);
|
||||||
const settingsState = { backend: "libretranslate", ltUrl: DEFAULT_LT_URL, ltKey: "" };
|
const settingsState = { backend: "libretranslate", ltUrl: DEFAULT_LT_URL, ltKey: "" };
|
||||||
const uiState = { src: "auto", tgt: defaultTarget };
|
const uiState = { src: "auto", tgt: defaultTarget, zoom: 100 };
|
||||||
|
|
||||||
|
// ---- text zoom ---------------------------------------------------------
|
||||||
|
// Percent of the 15px base, clamped so the panes stay usable at either end.
|
||||||
|
const ZOOM_MIN = 70, ZOOM_MAX = 220, ZOOM_STEP = 10, ZOOM_BASE = 15;
|
||||||
|
function applyZoom() {
|
||||||
|
const z = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, uiState.zoom || 100));
|
||||||
|
uiState.zoom = z;
|
||||||
|
document.documentElement.style.setProperty("--tsize", (ZOOM_BASE * z / 100).toFixed(2) + "px");
|
||||||
|
$("zoomPct").textContent = z + "%";
|
||||||
|
$("zoomOut").disabled = z <= ZOOM_MIN;
|
||||||
|
$("zoomIn").disabled = z >= ZOOM_MAX;
|
||||||
|
}
|
||||||
|
function setZoom(z) { uiState.zoom = z; applyZoom(); saveUi(); }
|
||||||
|
|
||||||
async function loadState() {
|
async function loadState() {
|
||||||
if (!S) return;
|
if (!S) return;
|
||||||
|
|
@ -196,6 +238,7 @@
|
||||||
ltKey.value = settingsState.ltKey;
|
ltKey.value = settingsState.ltKey;
|
||||||
srcSel.value = uiState.src;
|
srcSel.value = uiState.src;
|
||||||
tgtSel.value = uiState.tgt;
|
tgtSel.value = uiState.tgt;
|
||||||
|
applyZoom();
|
||||||
onBackendChange();
|
onBackendChange();
|
||||||
}
|
}
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
|
|
@ -293,12 +336,38 @@
|
||||||
backendSel.addEventListener("change", async () => { onBackendChange(); await saveSettings(); });
|
backendSel.addEventListener("change", async () => { onBackendChange(); await saveSettings(); });
|
||||||
ltUrl.addEventListener("change", saveSettings);
|
ltUrl.addEventListener("change", saveSettings);
|
||||||
ltKey.addEventListener("change", saveSettings);
|
ltKey.addEventListener("change", saveSettings);
|
||||||
srcSel.addEventListener("change", saveUi);
|
// Changing either language re-runs the translation straight away. Picking a
|
||||||
tgtSel.addEventListener("change", saveUi);
|
// new target and then having to hit Translate was the single most confusing
|
||||||
|
// thing about this panel: the old handler saved the choice and stopped
|
||||||
|
// there, so the pane below kept showing the previous language's text with
|
||||||
|
// no sign it was stale.
|
||||||
|
async function onLangChange() {
|
||||||
|
await saveUi();
|
||||||
|
if (!inp.value.trim()) return; // nothing to re-translate
|
||||||
|
const src = srcSel.value, tgt = tgtSel.value;
|
||||||
|
if (src !== "auto" && src === tgt) return; // doTranslate would just echo
|
||||||
|
doTranslate();
|
||||||
|
}
|
||||||
|
srcSel.addEventListener("change", onLangChange);
|
||||||
|
tgtSel.addEventListener("change", onLangChange);
|
||||||
|
|
||||||
// Ctrl/Cmd+Enter to translate.
|
// Zoom controls.
|
||||||
inp.addEventListener("keydown", (e) => {
|
$("zoomIn").addEventListener("click", () => setZoom((uiState.zoom || 100) + ZOOM_STEP));
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); doTranslate(); }
|
$("zoomOut").addEventListener("click", () => setZoom((uiState.zoom || 100) - ZOOM_STEP));
|
||||||
|
$("zoomPct").addEventListener("click", () => setZoom(100));
|
||||||
|
$("zoomPct").addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setZoom(100); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ctrl/Cmd+Enter to translate; Ctrl/Cmd +/-/0 to zoom the text panes.
|
||||||
|
// Bound on the document so it works whichever pane has focus.
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (!(e.ctrlKey || e.metaKey)) return;
|
||||||
|
if (e.key === "Enter" && e.target === inp) { e.preventDefault(); doTranslate(); return; }
|
||||||
|
// "=" is the unshifted "+" on most layouts; accept both.
|
||||||
|
if (e.key === "+" || e.key === "=") { e.preventDefault(); setZoom((uiState.zoom || 100) + ZOOM_STEP); }
|
||||||
|
else if (e.key === "-") { e.preventDefault(); setZoom((uiState.zoom || 100) - ZOOM_STEP); }
|
||||||
|
else if (e.key === "0") { e.preventDefault(); setZoom(100); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pending selection from a right-click. When the addon dispatches
|
// Pending selection from a right-click. When the addon dispatches
|
||||||
|
|
@ -321,7 +390,8 @@
|
||||||
// becomes visible again (the sidebar preload dispatches a "sidebar-
|
// becomes visible again (the sidebar preload dispatches a "sidebar-
|
||||||
// visibility" event on its window).
|
// visibility" event on its window).
|
||||||
(async () => {
|
(async () => {
|
||||||
await loadState();
|
applyZoom(); // paint the control before storage answers (or if it never does)
|
||||||
|
await loadState(); // re-applies with the saved zoom once it's read
|
||||||
await drainPending();
|
await drainPending();
|
||||||
inp.focus();
|
inp.focus();
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue