1
0

Separate strings.js file
All checks were successful
Build and test project / build-and-test (push) Successful in 1m53s

This commit is contained in:
2026-03-19 21:40:10 +01:00
parent 093f4f8d80
commit 9355da1796
4 changed files with 120 additions and 60 deletions

View File

@@ -10,9 +10,14 @@ const srcDir = join(webDir, 'src');
const distDir = join(webDir, 'dist'); const distDir = join(webDir, 'dist');
const isDev = process.argv.includes('--dev'); const isDev = process.argv.includes('--dev');
// Clean dist/ // Clean dist/ (preserve wasm/ which is built separately)
if (existsSync(distDir)) rmSync(distDir, { recursive: true }); if (existsSync(distDir)) {
mkdirSync(distDir, { recursive: true }); for (const entry of readdirSync(distDir)) {
if (entry !== 'wasm') {
rmSync(join(distDir, entry), { recursive: true, force: true });
}
}
}
// Build JS bundle // Build JS bundle
await esbuild.build({ await esbuild.build({

View File

@@ -3,6 +3,7 @@ import { loadSoftwareUrls, getSoftwareUrl, getDevicesForVersion } from './kobo-s
import { PatchUI, scanAvailablePatches } from './patch-ui.js'; import { PatchUI, scanAvailablePatches } from './patch-ui.js';
import { KoboPatchRunner } from './patch-runner.js'; import { KoboPatchRunner } from './patch-runner.js';
import { NickelMenuInstaller } from './nickelmenu.js'; import { NickelMenuInstaller } from './nickelmenu.js';
import { TL } from './strings.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
(() => { (() => {
@@ -130,11 +131,8 @@ import JSZip from 'jszip';
]; ];
// --- Step navigation --- // --- Step navigation ---
const NAV_NICKELMENU = ['Device', 'Mode', 'Configure', 'Review', 'Install'];
const NAV_PATCHES = ['Device', 'Mode', 'Patches', 'Build', 'Install'];
const NAV_DEFAULT = ['Device', 'Mode', 'Patches', 'Build', 'Install'];
let currentNavLabels = NAV_DEFAULT; let currentNavLabels = TL.NAV_DEFAULT;
const stepHistory = [stepConnect]; const stepHistory = [stepConnect];
@@ -204,9 +202,9 @@ import JSZip from 'jszip';
const count = patchUI.getEnabledCount(); const count = patchUI.getEnabledCount();
btnPatchesNext.disabled = false; btnPatchesNext.disabled = false;
if (count === 0) { if (count === 0) {
patchCountHint.textContent = 'No patches selected \u2014 continuing will restore the original unpatched software.'; patchCountHint.textContent = TL.STATUS.PATCH_COUNT_ZERO;
} else { } else {
patchCountHint.textContent = count === 1 ? '1 patch selected.' : count + ' patches selected.'; patchCountHint.textContent = count === 1 ? TL.STATUS.PATCH_COUNT_ONE : TL.STATUS.PATCH_COUNT_MULTI(count);
} }
} }
@@ -232,7 +230,7 @@ import JSZip from 'jszip';
$('connect-unsupported-hint').hidden = false; $('connect-unsupported-hint').hidden = false;
} }
setNavLabels(NAV_DEFAULT); setNavLabels(TL.NAV_DEFAULT);
setNavStep(1); setNavStep(1);
showStep(stepConnect); showStep(stepConnect);
@@ -280,7 +278,7 @@ import JSZip from 'jszip';
try { try {
const loaded = await loadPatchesForVersion(version, availablePatches); const loaded = await loadPatchesForVersion(version, availablePatches);
if (!loaded) { if (!loaded) {
showError('Could not load patches for software version ' + version); showError(TL.ERROR.LOAD_PATCHES_FAILED(version));
return; return;
} }
configureFirmwareStep(version, selectedPrefix); configureFirmwareStep(version, selectedPrefix);
@@ -345,7 +343,7 @@ import JSZip from 'jszip';
deviceUnknownCheckbox.checked = false; deviceUnknownCheckbox.checked = false;
btnDeviceNext.disabled = true; btnDeviceNext.disabled = true;
} else { } else {
deviceStatus.textContent = 'Your device has been recognized. You can continue to the next step!'; deviceStatus.textContent = TL.STATUS.DEVICE_RECOGNIZED;
deviceUnknownWarning.hidden = true; deviceUnknownWarning.hidden = true;
deviceUnknownAck.hidden = true; deviceUnknownAck.hidden = true;
deviceUnknownCheckbox.checked = false; deviceUnknownCheckbox.checked = false;
@@ -372,7 +370,7 @@ import JSZip from 'jszip';
if (!patchesLoaded) return; if (!patchesLoaded) return;
selectedMode = 'patches'; selectedMode = 'patches';
isRestore = true; isRestore = true;
setNavLabels(NAV_PATCHES); setNavLabels(TL.NAV_PATCHES);
goToBuild(); goToBuild();
}); });
@@ -410,7 +408,7 @@ import JSZip from 'jszip';
patchesHint.hidden = true; patchesHint.hidden = true;
} }
setNavLabels(NAV_DEFAULT); setNavLabels(TL.NAV_DEFAULT);
setNavStep(2); setNavStep(2);
showStep(stepMode); showStep(stepMode);
} }
@@ -430,14 +428,14 @@ import JSZip from 'jszip';
selectedMode = selected.value; selectedMode = selected.value;
if (selectedMode === 'nickelmenu') { if (selectedMode === 'nickelmenu') {
setNavLabels(NAV_NICKELMENU); setNavLabels(TL.NAV_NICKELMENU);
goToNickelMenuConfig(); goToNickelMenuConfig();
} else if (manualMode && !patchesLoaded) { } else if (manualMode && !patchesLoaded) {
// Manual mode: need version/model selection before patches // Manual mode: need version/model selection before patches
setNavLabels(NAV_PATCHES); setNavLabels(TL.NAV_PATCHES);
await enterManualVersionSelection(); await enterManualVersionSelection();
} else { } else {
setNavLabels(NAV_PATCHES); setNavLabels(TL.NAV_PATCHES);
goToPatches(); goToPatches();
} }
}); });
@@ -481,7 +479,7 @@ import JSZip from 'jszip';
await addsDir.getDirectoryHandle('nm'); await addsDir.getDirectoryHandle('nm');
removeRadio.disabled = false; removeRadio.disabled = false;
removeOption.classList.remove('nm-option-disabled'); removeOption.classList.remove('nm-option-disabled');
removeDesc.textContent = 'Removes NickelMenu from your device. You must restart your Kobo to complete the uninstall!'; removeDesc.textContent = TL.STATUS.NM_REMOVAL_HINT;
return; return;
} catch { } catch {
// .adds/nm not found // .adds/nm not found
@@ -490,7 +488,7 @@ import JSZip from 'jszip';
removeRadio.disabled = true; removeRadio.disabled = true;
removeOption.classList.add('nm-option-disabled'); removeOption.classList.add('nm-option-disabled');
removeDesc.textContent = 'Removes NickelMenu from your device. Only available when a Kobo with NickelMenu installed is connected.'; removeDesc.textContent = TL.STATUS.NM_REMOVAL_DISABLED;
if (removeRadio.checked) { if (removeRadio.checked) {
const sampleRadio = $q('input[value="sample"]', stepNickelMenu); const sampleRadio = $q('input[value="sample"]', stepNickelMenu);
sampleRadio.checked = true; sampleRadio.checked = true;
@@ -540,33 +538,33 @@ import JSZip from 'jszip';
list.innerHTML = ''; list.innerHTML = '';
if (nickelMenuOption === 'remove') { if (nickelMenuOption === 'remove') {
summary.textContent = 'NickelMenu will be updated and marked for removal. It will uninstall itself when your Kobo reboots.'; summary.textContent = TL.STATUS.NM_WILL_BE_REMOVED;
btnNmWrite.hidden = manualMode; btnNmWrite.hidden = manualMode;
btnNmWrite.textContent = 'Remove from Kobo'; btnNmWrite.textContent = TL.BUTTON.REMOVE_FROM_KOBO;
btnNmDownload.hidden = true; btnNmDownload.hidden = true;
} else if (nickelMenuOption === 'nickelmenu-only') { } else if (nickelMenuOption === 'nickelmenu-only') {
summary.textContent = 'The following will be installed on your Kobo:'; summary.textContent = TL.STATUS.NM_WILL_BE_INSTALLED;
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = 'NickelMenu (KoboRoot.tgz)'; li.textContent = TL.STATUS.NM_NICKEL_ROOT_TGZ;
list.appendChild(li); list.appendChild(li);
btnNmWrite.hidden = false; btnNmWrite.hidden = false;
btnNmWrite.textContent = 'Write to Kobo'; btnNmWrite.textContent = TL.BUTTON.WRITE_TO_KOBO;
btnNmDownload.hidden = false; btnNmDownload.hidden = false;
} else { } else {
summary.textContent = 'The following will be installed on your Kobo:'; summary.textContent = TL.STATUS.NM_WILL_BE_INSTALLED;
const items = ['NickelMenu (KoboRoot.tgz)', 'Custom menu configuration']; const items = [TL.STATUS.NM_NICKEL_ROOT_TGZ, 'Custom menu configuration'];
const cfg = getNmConfig(); const cfg = getNmConfig();
if (cfg.fonts) items.push('Readerly fonts'); if (cfg.fonts) items.push(TL.NICKEL_MENU_ITEMS.FONTS);
if (cfg.screensaver) items.push('Custom screensaver'); if (cfg.screensaver) items.push(TL.NICKEL_MENU_ITEMS.SCREENSAVER);
if (cfg.simplifyTabs) items.push('Simplified tab menu'); if (cfg.simplifyTabs) items.push(TL.NICKEL_MENU_ITEMS.SIMPLIFY_TABS);
if (cfg.simplifyHome) items.push('Simplified homescreen'); if (cfg.simplifyHome) items.push(TL.NICKEL_MENU_ITEMS.SIMPLIFY_HOME);
for (const text of items) { for (const text of items) {
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = text; li.textContent = text;
list.appendChild(li); list.appendChild(li);
} }
btnNmWrite.hidden = false; btnNmWrite.hidden = false;
btnNmWrite.textContent = 'Write to Kobo'; btnNmWrite.textContent = TL.BUTTON.WRITE_TO_KOBO;
btnNmDownload.hidden = false; btnNmDownload.hidden = false;
} }
@@ -617,7 +615,7 @@ import JSZip from 'jszip';
showNmDone('download'); showNmDone('download');
} }
} catch (err) { } catch (err) {
showError('NickelMenu installation failed: ' + err.message); showError(TL.STATUS.NM_INSTALL_FAILED + err.message);
} }
} }
@@ -631,13 +629,13 @@ import JSZip from 'jszip';
$('nm-reboot-instructions').hidden = true; $('nm-reboot-instructions').hidden = true;
if (mode === 'remove') { if (mode === 'remove') {
nmDoneStatus.textContent = 'NickelMenu will be removed on next reboot.'; nmDoneStatus.textContent = TL.STATUS.NM_REMOVED_ON_REBOOT;
$('nm-reboot-instructions').hidden = false; $('nm-reboot-instructions').hidden = false;
} else if (mode === 'written') { } else if (mode === 'written') {
nmDoneStatus.textContent = 'NickelMenu has been installed on your Kobo.'; nmDoneStatus.textContent = TL.STATUS.NM_INSTALLED;
$('nm-write-instructions').hidden = false; $('nm-write-instructions').hidden = false;
} else { } else {
nmDoneStatus.textContent = 'Your NickelMenu package is ready to download.'; nmDoneStatus.textContent = TL.STATUS.NM_DOWNLOAD_READY;
triggerDownload(resultNmZip, 'NickelMenu-install.zip', 'application/zip'); triggerDownload(resultNmZip, 'NickelMenu-install.zip', 'application/zip');
$('nm-download-instructions').hidden = false; $('nm-download-instructions').hidden = false;
// Show eReader.conf step only when sample config is included // Show eReader.conf step only when sample config is included
@@ -689,13 +687,11 @@ import JSZip from 'jszip';
function goToBuild() { function goToBuild() {
if (isRestore) { if (isRestore) {
firmwareDescription.textContent = firmwareDescription.textContent = TL.STATUS.RESTORE_ORIGINAL;
'will be downloaded and extracted without modifications to restore the original unpatched software.'; btnBuild.textContent = TL.BUTTON.RESTORE_ORIGINAL;
btnBuild.textContent = 'Restore Original Software';
} else { } else {
firmwareDescription.textContent = firmwareDescription.textContent = TL.STATUS.FIRMWARE_WILL_BE_DOWNLOADED;
'will be downloaded automatically from Kobo\u2019s servers and will be patched after the download completes.'; btnBuild.textContent = TL.BUTTON.BUILD_PATCHED;
btnBuild.textContent = 'Build Patched Software';
} }
populateSelectedPatchesList(); populateSelectedPatchesList();
setNavStep(4); setNavStep(4);
@@ -705,7 +701,7 @@ import JSZip from 'jszip';
btnBuildBack.addEventListener('click', () => { btnBuildBack.addEventListener('click', () => {
if (isRestore) { if (isRestore) {
isRestore = false; isRestore = false;
setNavLabels(NAV_DEFAULT); setNavLabels(TL.NAV_DEFAULT);
setNavStep(1); setNavStep(1);
showStep(stepDevice); showStep(stepDevice);
} else { } else {
@@ -729,7 +725,7 @@ import JSZip from 'jszip';
const contentLength = resp.headers.get('Content-Length'); const contentLength = resp.headers.get('Content-Length');
if (!contentLength || !resp.body) { if (!contentLength || !resp.body) {
buildProgress.textContent = 'Downloading software update...'; buildProgress.textContent = TL.STATUS.DOWNLOADING;
return new Uint8Array(await resp.arrayBuffer()); return new Uint8Array(await resp.arrayBuffer());
} }
@@ -744,8 +740,7 @@ import JSZip from 'jszip';
chunks.push(value); chunks.push(value);
received += value.length; received += value.length;
const pct = ((received / total) * 100).toFixed(0); const pct = ((received / total) * 100).toFixed(0);
buildProgress.textContent = buildProgress.textContent = TL.STATUS.DOWNLOADING_PROGRESS(formatMB(received), formatMB(total), pct);
`Downloading software update... ${formatMB(received)} / ${formatMB(total)} (${pct}%)`;
} }
const result = new Uint8Array(received); const result = new Uint8Array(received);
@@ -758,18 +753,18 @@ import JSZip from 'jszip';
} }
async function extractOriginalTgz(firmwareBytes) { async function extractOriginalTgz(firmwareBytes) {
buildProgress.textContent = 'Extracting KoboRoot.tgz...'; buildProgress.textContent = TL.STATUS.EXTRACTING;
appendLog('Extracting original KoboRoot.tgz from software update...'); appendLog('Extracting original KoboRoot.tgz from firmware...');
const zip = await JSZip.loadAsync(firmwareBytes); const zip = await JSZip.loadAsync(firmwareBytes);
const koboRoot = zip.file('KoboRoot.tgz'); const koboRoot = zip.file('KoboRoot.tgz');
if (!koboRoot) throw new Error('KoboRoot.tgz not found in software update'); if (!koboRoot) throw new Error(TL.STATUS.EXTRACT_FAILED);
const tgz = new Uint8Array(await koboRoot.async('arraybuffer')); const tgz = new Uint8Array(await koboRoot.async('arraybuffer'));
appendLog('Extracted KoboRoot.tgz: ' + formatMB(tgz.length)); appendLog('Extracted KoboRoot.tgz: ' + formatMB(tgz.length));
return tgz; return tgz;
} }
async function runPatcher(firmwareBytes) { async function runPatcher(firmwareBytes) {
buildProgress.textContent = 'Applying patches...'; buildProgress.textContent = TL.STATUS.APPLYING_PATCHES;
const configYAML = patchUI.generateConfig(); const configYAML = patchUI.generateConfig();
const patchFiles = patchUI.getPatchFileBytes(); const patchFiles = patchUI.getPatchFileBytes();
@@ -804,7 +799,7 @@ import JSZip from 'jszip';
btnWrite.hidden = manualMode; btnWrite.hidden = manualMode;
btnWrite.disabled = false; btnWrite.disabled = false;
btnWrite.className = 'primary'; btnWrite.className = 'primary';
btnWrite.textContent = 'Write to Kobo'; btnWrite.textContent = TL.BUTTON.WRITE_TO_DEVICE;
btnDownload.disabled = false; btnDownload.disabled = false;
writeInstructions.hidden = true; writeInstructions.hidden = true;
downloadInstructions.hidden = true; downloadInstructions.hidden = true;
@@ -832,14 +827,14 @@ import JSZip from 'jszip';
btnBuild.addEventListener('click', async () => { btnBuild.addEventListener('click', async () => {
showStep(stepBuilding, false); showStep(stepBuilding, false);
buildLog.textContent = ''; buildLog.textContent = '';
buildProgress.textContent = 'Starting...'; buildProgress.textContent = TL.STATUS.BUILDING_STARTING;
$('build-wait-hint').textContent = isRestore $('build-wait-hint').textContent = isRestore
? 'Please wait while the original software is being downloaded and extracted...' ? 'Please wait while the original software is being downloaded and extracted...'
: 'Please wait while the patch is being applied...'; : 'Please wait while the patch is being applied...';
try { try {
if (!firmwareURL) { if (!firmwareURL) {
showError('No download URL available for this device.'); showError(TL.STATUS.NO_FIRMWARE_URL);
return; return;
} }
@@ -862,7 +857,7 @@ import JSZip from 'jszip';
if (!resultTgz || !device.directoryHandle) return; if (!resultTgz || !device.directoryHandle) return;
btnWrite.disabled = true; btnWrite.disabled = true;
btnWrite.textContent = 'Writing...'; btnWrite.textContent = TL.BUTTON.WRITING;
downloadInstructions.hidden = true; downloadInstructions.hidden = true;
try { try {
@@ -872,13 +867,13 @@ import JSZip from 'jszip';
await writable.write(resultTgz); await writable.write(resultTgz);
await writable.close(); await writable.close();
btnWrite.textContent = 'Written'; btnWrite.textContent = TL.BUTTON.WRITTEN;
btnWrite.className = 'btn-success'; btnWrite.className = 'btn-success';
writeInstructions.hidden = false; writeInstructions.hidden = false;
} catch (err) { } catch (err) {
btnWrite.disabled = false; btnWrite.disabled = false;
btnWrite.textContent = 'Write to Kobo'; btnWrite.textContent = TL.BUTTON.WRITE_TO_DEVICE;
showError('Failed to write KoboRoot.tgz: ' + err.message); showError(TL.STATUS.WRITE_FAILED + err.message);
} }
}); });
@@ -905,12 +900,12 @@ import JSZip from 'jszip';
const hasBackStep = stepHistory.includes(stepPatches); const hasBackStep = stepHistory.includes(stepPatches);
if (hasBackStep) { if (hasBackStep) {
errorTitle.textContent = 'The patch failed to apply'; errorTitle.textContent = TL.ERROR.PATCH_FAILED;
errorHint.hidden = false; errorHint.hidden = false;
btnErrorBack.hidden = false; btnErrorBack.hidden = false;
btnRetry.classList.add('danger'); btnRetry.classList.add('danger');
} else { } else {
errorTitle.textContent = 'Something went wrong'; errorTitle.textContent = TL.ERROR.SOMETHING_WENT_WRONG;
errorHint.hidden = true; errorHint.hidden = true;
btnErrorBack.hidden = true; btnErrorBack.hidden = true;
btnRetry.classList.remove('danger'); btnRetry.classList.remove('danger');
@@ -944,7 +939,7 @@ import JSZip from 'jszip';
btnDeviceNext.hidden = false; btnDeviceNext.hidden = false;
btnDeviceRestore.hidden = false; btnDeviceRestore.hidden = false;
setNavLabels(NAV_DEFAULT); setNavLabels(TL.NAV_DEFAULT);
setNavStep(1); setNavStep(1);
showStep(stepConnect); showStep(stepConnect);
}); });

View File

@@ -1,4 +1,5 @@
import JSZip from 'jszip'; import JSZip from 'jszip';
import { TL } from './strings.js';
/** /**
* Friendly display names for patch files. * Friendly display names for patch files.
@@ -288,7 +289,7 @@ class PatchUI {
}); });
const noneName = document.createElement('span'); const noneName = document.createElement('span');
noneName.className = 'patch-name patch-name-none'; noneName.className = 'patch-name patch-name-none';
noneName.textContent = 'None (do not patch)'; noneName.textContent = TL.PATCH.NONE;
noneHeader.appendChild(noneInput); noneHeader.appendChild(noneInput);
noneHeader.appendChild(noneName); noneHeader.appendChild(noneName);
noneItem.appendChild(noneHeader); noneItem.appendChild(noneHeader);

59
web/src/js/strings.js Normal file
View File

@@ -0,0 +1,59 @@
export const TL = {
NAV_NICKELMENU: ['Device', 'Mode', 'Configure', 'Review', 'Install'],
NAV_PATCHES: ['Device', 'Mode', 'Patches', 'Build', 'Install'],
NAV_DEFAULT: ['Device', 'Mode', 'Patches', 'Build', 'Install'],
BUTTON: {
RESTORE_ORIGINAL: 'Restore Original Software',
BUILD_PATCHED: 'Build Patched Software',
WRITE_TO_DEVICE: 'Write to Kobo',
REMOVE_FROM_KOBO: 'Remove from Kobo',
WRITING: 'Writing...',
WRITTEN: 'Written',
GO_BACK: '\u2039 Back',
SELECT_DIFFERENT_PATCHES: '\u2039 Select different patches',
},
STATUS: {
DEVICE_RECOGNIZED: 'Your device has been recognized. You can continue to the next step!',
NM_REMOVED_ON_REBOOT: 'NickelMenu will be removed on next reboot.',
NM_INSTALLED: 'NickelMenu has been installed on your Kobo.',
NM_DOWNLOAD_READY: 'Your NickelMenu package is ready to download.',
NM_WILL_BE_REMOVED: 'NickelMenu will be updated and marked for removal. It will uninstall itself when your Kobo reboots.',
NM_WILL_BE_INSTALLED: 'The following will be installed on your Kobo:',
NM_NICKEL_ROOT_TGZ: 'NickelMenu (KoboRoot.tgz)',
NM_REMOVAL_HINT: 'Removes NickelMenu from your device. You must restart your Kobo to complete the uninstall!',
NM_REMOVAL_DISABLED: 'Removes NickelMenu from your device. Only available when a Kobo with NickelMenu installed is connected.',
PATCH_COUNT_ZERO: 'No patches selected \u2014 continuing will restore the original unpatched software.',
PATCH_COUNT_ONE: '1 patch selected.',
PATCH_COUNT_MULTI: (n) => `${n} patches selected.`,
FIRMWARE_WILL_BE_DOWNLOADED: 'will be downloaded automatically from Kobo\u2019s servers and will be patched after the download completes.',
RESTORE_ORIGINAL: 'will be downloaded and extracted without modifications to restore the original unpatched software.',
BUILDING_STARTING: 'Starting...',
DOWNLOADING: 'Downloading software update...',
DOWNLOADING_PROGRESS: (received, total, pct) => `Downloading software update... ${received} / ${total} (${pct}%)`,
EXTRACTING: 'Extracting KoboRoot.tgz...',
APPLYING_PATCHES: 'Applying patches...',
NO_FIRMWARE_URL: 'No download URL available for this device.',
WRITE_FAILED: 'Failed to write KoboRoot.tgz: ',
NM_INSTALL_FAILED: 'NickelMenu installation failed: ',
EXTRACT_FAILED: 'KoboRoot.tgz not found in software update',
},
ERROR: {
PATCH_FAILED: 'The patch failed to apply',
SOMETHING_WENT_WRONG: 'Something went wrong',
LOAD_PATCHES_FAILED: (v) => `Could not load patches for software version ${v}`,
},
PATCH: {
NONE: 'None (do not patch)',
},
NICKEL_MENU_ITEMS: {
FONTS: 'Readerly fonts',
SCREENSAVER: 'Custom screensaver',
SIMPLIFY_TABS: 'Simplified tab menu',
SIMPLIFY_HOME: 'Simplified homescreen',
},
};