Skip to content

Commit 2d06d25

Browse files
committed
webui: add multi-language support, add built-in help menu
1 parent b60e9fa commit 2d06d25

8 files changed

Lines changed: 426 additions & 60 deletions

File tree

webui/assets/locales.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
const rtlLang = [
2+
'ar', // Arabic
3+
'fa', // Persian
4+
'he', // Hebrew
5+
'ur', // Urdu
6+
'ps', // Pashto
7+
'sd', // Sindhi
8+
'ku', // Kurdish
9+
'yi', // Yiddish
10+
'dv', // Dhivehi
11+
];
12+
13+
export let translations = {};
14+
let baseTranslations = {};
15+
let availableLanguages = ['en'];
16+
let languageNames = {};
17+
18+
/**
19+
* Parse XML translation file into a JavaScript object
20+
* @param {string} xmlText - The XML content as string
21+
* @returns {Object} - Parsed translations
22+
*/
23+
function parseTranslationsXML(xmlText) {
24+
const parser = new DOMParser();
25+
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
26+
const strings = xmlDoc.getElementsByTagName('string');
27+
const translations = {};
28+
29+
for (let i = 0; i < strings.length; i++) {
30+
const string = strings[i];
31+
const name = string.getAttribute('name');
32+
const value = string.textContent;
33+
translations[name] = value;
34+
}
35+
36+
return translations;
37+
}
38+
39+
/**
40+
* Detect user's default language
41+
* @returns {Promise<string>} - Detected language code
42+
*/
43+
async function detectUserLanguage() {
44+
const userLang = navigator.language || navigator.userLanguage;
45+
const langCode = userLang.split('-')[0];
46+
47+
try {
48+
// Fetch available languages
49+
const availableResponse = await fetch('locales/languages.json');
50+
const availableData = await availableResponse.json();
51+
availableLanguages = Object.keys(availableData);
52+
languageNames = availableData;
53+
54+
// Check if preferred language is valid
55+
if (availableLanguages.includes(userLang)) {
56+
return userLang;
57+
} else if (availableLanguages.includes(langCode)) {
58+
return langCode;
59+
} else {
60+
return 'en';
61+
}
62+
} catch (e) {
63+
return 'en';
64+
}
65+
}
66+
67+
/**
68+
* Load translations dynamically based on the selected language
69+
* @returns {Promise<void>}
70+
*/
71+
export async function loadTranslations() {
72+
try {
73+
// load Englsih as base translations
74+
const baseResponse = await fetch('./locales/strings/en.xml');
75+
const baseXML = await baseResponse.text();
76+
baseTranslations = parseTranslationsXML(baseXML);
77+
78+
// load user's language if available
79+
const lang = await detectUserLanguage();
80+
if (lang !== 'en') {
81+
const response = await fetch(`locales/strings/${lang}.xml`);
82+
const userXML = await response.text();
83+
const userTranslations = parseTranslationsXML(userXML);
84+
translations = { ...baseTranslations, ...userTranslations };
85+
} else {
86+
translations = baseTranslations;
87+
}
88+
89+
// Support for rtl language
90+
const isRTL = rtlLang.includes(lang.split('-')[0]);
91+
document.documentElement.setAttribute('dir', isRTL ? 'rtl' : 'ltr');
92+
} catch (error) {
93+
translations = baseTranslations;
94+
}
95+
applyTranslations();
96+
}
97+
98+
/**
99+
* Apply translations to all elements with data-i18n attributes
100+
* @returns {void}
101+
*/
102+
function applyTranslations() {
103+
document.querySelectorAll("[data-i18n]").forEach((el) => {
104+
const key = el.getAttribute("data-i18n");
105+
const translation = translations[key];
106+
if (translation) {
107+
if (el.hasAttribute("placeholder")) {
108+
el.setAttribute("placeholder", translation);
109+
} else if (el.hasAttribute("label")) {
110+
el.setAttribute("label", translation);
111+
} else {
112+
el.textContent = translation;
113+
}
114+
}
115+
});
116+
}

webui/assets/scripts.js

Lines changed: 55 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { exec, spawn, toast } from "kernelsu-alt";
22
import '@material/web/all.js';
3+
import { translations, loadTranslations } from './locales.js';
34

45
let scriptOnly = false;
56
let shellRunning = false;
@@ -54,6 +55,8 @@ function applyButtonEventListeners() {
5455
const confirmFetchBtn = document.getElementById('confirm-fetch');
5556
const githubBtn = document.getElementById('github-btn');
5657
const helpBtn = document.getElementById('help-btn');
58+
const helpDialog = document.getElementById('help-dialog');
59+
const romSignCheck = document.getElementById('rom-sign-check');
5760

5861
fetchBtn.onclick = () => {
5962
if (randomRadio.checked) randomRadio.checked = false;
@@ -84,21 +87,21 @@ function applyButtonEventListeners() {
8487
lines.forEach(line => appendToOutput(line));
8588
appendToOutput("");
8689
} else {
87-
appendToOutput(`[!] Failed to read pif.prop: ${result.stderr}`, true);
90+
appendToOutput(`[!] ${translations.output_error_read_pif_prop}: ${result.stderr}`, true);
8891
}
8992
}
9093

9194
securityPatchBtn.onclick = async () => {
9295
await exec(`sh ${moddir}/security_patch.sh --${securityPatchBtn.selected ? 'enable' : 'disable'}`);
9396
await loadAutoSecurityPatchConfig();
94-
appendToOutput(`[+] ${securityPatchBtn.selected ? 'Enabled' : 'Disbled'} auto security patch.`);
97+
appendToOutput(`[+] ${securityPatchBtn.selected ? translations.output_enabled : translations.output_disabled} auto security patch.`);
9598
}
9699

97100
scriptOnlyBtn.onclick = async () => {
98101
await exec(`${scriptOnly ? 'rm -rf /data/adb/pif_script_only' : 'touch /data/adb/pif_script_only'} || true`);
99102
killGms();
100103
loadScriptOnlyConfig();
101-
appendToOutput(`[+] ${scriptOnly ? 'Disabled' : 'Enabled'} script only mode.`);
104+
appendToOutput(`[+] ${scriptOnly ? translations.output_disabled : translations.output_enabled} script only mode.`);
102105
}
103106

104107
confirmFetchBtn.addEventListener('click', (e) => {
@@ -143,7 +146,19 @@ function applyButtonEventListeners() {
143146
});
144147

145148
githubBtn.onclick = () => linkRedirect(`https://github.com/${repository}/releases/latest`);
146-
helpBtn.onclick = () => linkRedirect(`https://github.com/${repository}#options`);
149+
helpBtn.onclick = () => helpDialog.show();
150+
151+
romSignCheck.onclick = () => {
152+
const command = romSignCheck.parentElement.querySelector('code').textContent;
153+
appendToOutput(command);
154+
appendToOutput('');
155+
exec(command).then((result) => {
156+
const isSuccess = result.errno === 0;
157+
setTimeout(() => {
158+
appendToOutput(isSuccess ? result.stdout : result.stderr, !isSuccess);
159+
}, 600);
160+
});
161+
}
147162
}
148163

149164
function linkRedirect(link) {
@@ -188,8 +203,8 @@ async function loadSpoofConfig() {
188203

189204
if (model === null) model = pifMap.MODEL;
190205
} catch (error) {
191-
appendToOutput(`[!] Failed to load spoof config: ${error}`, true);
192-
appendToOutput('[!] Warning: Do not use third party tools to fetch pif.prop');
206+
appendToOutput(`[!] ${translations.output_error_load_spoof_config}: ${error}`, true);
207+
appendToOutput('[!] ' + translations.output_warning_third_party_tool);
193208
resetPifProp();
194209
}
195210
}
@@ -202,7 +217,7 @@ function resetPifProp() {
202217
return response.text();
203218
})
204219
.catch(error => {
205-
return fetch(`https://raw.gitmirror.com/${repository}/${branch}/module/pif.prop`)
220+
return fetch(`https://hub.gitmirror.com/raw.githubusercontent.com/${repository}/${branch}/module/pif.prop`)
206221
.then(response => {
207222
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
208223
return response.text();
@@ -212,16 +227,16 @@ function resetPifProp() {
212227
const pifProp = text.trim();
213228
const { errno, stderr } = await exec(`
214229
echo '${pifProp}' > ${moddir}/pif.prop
215-
rm -f /data/adb/pif.prop || true
230+
rm -f /data/adb/pif.prop
216231
`);
217232
if (errno === 0) {
218-
appendToOutput(`[+] Successfully reset pif.prop`);
233+
appendToOutput('[+] ' + translations.output_reset_pif_prop);
219234
} else {
220-
appendToOutput(`[!] Failed to reset pif.prop: ${stderr}`, true);
235+
throw new Error(stderr);
221236
}
222237
})
223238
.catch(error => {
224-
appendToOutput(`[!] Failed to reset pif.prop: ${error.message}`);
239+
appendToOutput(`[!] ${translations.output_error_reset_failed}: ${error.message}`, true);
225240
});
226241
}
227242

@@ -238,7 +253,6 @@ function setupSpoofConfigButton() {
238253
[ ! -f /data/adb/pif.prop ] || echo "/data/adb/pif.prop"
239254
`);
240255
result.stdout.on('data', (data) => pifFile += data + '\n');
241-
result.stderr.on('data', (data) => appendToOutput(`[!] Failed to find pif.prop: ` + data, true));
242256
result.on('exit', (code) => {
243257
if (code !== 0) return;
244258
updateSpoofConfig(toggle, item.config, pifFile);
@@ -284,34 +298,21 @@ function updateSpoofConfig(toggle, type, pifFile) {
284298
if (code === 0) {
285299
if (prompted) return;
286300
prompted = true;
287-
appendToOutput(`[+] ${toggle.selected ? "Enabled" : "Disabled"} ${type}`);
301+
appendToOutput(`[+] ${toggle.selected ? translations.output_enabled : translations.output_disabled} ${type}`);
288302
} else {
289303
throw new Error('Failed to write ' + pifFile);
290304
}
291305
});
292306

293307
// reminder
294308
if (!reminded && (type === "spoofVendingBuild" || type === "spoofVendingSdk") && pifMap.spoofVendingBuild && pifMap.spoofVendingSdk) {
295-
appendToOutput('[!] spoofVendingSdk will not take effect when spoofVendingBuild is enabled.');
309+
appendToOutput('[!] ' + translations.output_spoofVendingSdk_spoofVendingBuild);
296310
reminded = true;
297311
}
298-
299-
// reminder
300-
if (!reminded && type === "spoofSignature") {
301-
reminded = true;
302-
const signature = await exec('unzip -l /system/etc/security/otacerts.zip | grep -oE "testkey|releasekey"');
303-
if (signature.errno === 0) {
304-
if (signature.stdout.trim() === "testkey" && !pifMap.spoofSignature) {
305-
appendToOutput('[!] Unsigned ROM detected, enable spoofSignature to fix.');
306-
} else if (signature.stdout.trim() === "releasekey" && pifMap.spoofSignature) {
307-
appendToOutput('[+] Signed ROM detected, enabling spoofSignature might not be useful.');
308-
}
309-
}
310-
}
311312
});
312313
} catch (error) {
313314
console.error(`Failed to update ${pifFile}:`, error);
314-
appendToOutput(`[!] Failed to ${toggle.selected ? "enable" : "disable"} ${item.config}`);
315+
appendToOutput(`[!] ${toggle.selected ? output_error_enable_failed : output_error_disable_failed} ${item.config}`);
315316
}
316317
}
317318
}
@@ -344,29 +345,19 @@ function runAction() {
344345
if (model && product) opts = { env: { MODEL: `"${model}"`, PRODUCT: `"${product}"`} };
345346
const scriptOutput = spawn("sh", [`${moddir}/autopif.sh`], opts);
346347
scriptOutput.stdout.on('data', (data) => appendToOutput(data));
347-
scriptOutput.stderr.on('data', (data) => appendToOutput(`[!] Error executing autopif.sh: ${data}`, true));
348+
scriptOutput.stderr.on('data', (data) => appendToOutput(data, true));
348349
scriptOutput.on('exit', () => {
349350
appendToOutput("");
350351
muteToggle(false);
351352
});
352-
scriptOutput.on('error', () => {
353-
appendToOutput("[!] Error: Fail to execute autopif.sh", true);
354-
appendToOutput("");
355-
muteToggle(false);
356-
});
357353
}
358354

359355
function updateAutopif() {
360356
muteToggle(true);
361357
const scriptOutput = spawn("sh", [`${moddir}/autopif_ota.sh`]);
362358
scriptOutput.stdout.on('data', (data) => appendToOutput(data));
363-
scriptOutput.stderr.on('data', (data) => appendToOutput(`[!] Error executing autopif_ota.sh: ${data}`, true));
359+
scriptOutput.stderr.on('data', (data) => appendToOutput(data, true));
364360
scriptOutput.on('exit', () => muteToggle(false));
365-
scriptOutput.on('error', () => {
366-
appendToOutput("[!] Error: Fail to execute autopif_ota.sh", true);
367-
appendToOutput("");
368-
muteToggle(false);
369-
});
370361
}
371362

372363
function muteToggle(mute, scriptOnly = null) {
@@ -454,7 +445,7 @@ function loadScriptOnlyConfig() {
454445
}
455446

456447
function fetchPifProp() {
457-
appendToOutput("[+] Fetching pif.prop from GitHub...");
448+
appendToOutput("[+] " + translations.output_fetching_from_github);
458449
appendToOutput("");
459450
fetch(`https://raw.githubusercontent.com/${repository}/bot/device_prop/${product}.prop`)
460451
.then(response => {
@@ -489,13 +480,13 @@ fi
489480
if (result.errno === 0) {
490481
result.stdout.split('\n').forEach(line => appendToOutput(line));
491482
} else {
492-
appendToOutput("[!] Failed to write /data/adb/pif.prop: " + result.stderr, true);
483+
appendToOutput(`[!] ${translations.output_error_write_pif_prop}: ` + result.stderr, true);
493484
}
494485
killGms();
495486
});
496487
})
497488
.catch(error => {
498-
appendToOutput('[!] Failed to fetch pif.prop: ' + error, true);
489+
appendToOutput(`[!] ${translations.output_error_fetch_pif_prop}: ` + error, true);
499490
});
500491
}
501492

@@ -544,7 +535,7 @@ function setupDeviceList() {
544535
});
545536
})
546537
.catch(error => {
547-
list.querySelector('#device-list-loading').innerHTML = '<div>Failed to load device list</div>';
538+
list.querySelector('#device-list-loading').innerHTML = `<div>${translations.device_list_load_failed}</div>`;
548539
document.getElementById('confirm-fetch').disabled = true;
549540
});
550541
}
@@ -558,22 +549,37 @@ function checkPropDate() {
558549
different="$(($current_epoch - $prop_epoch))"
559550
if [ $different -gt 5184000 ]; then
560551
# 60d * 24h * 60m * 60s = 5184000
561-
echo "[!] pif.prop is likely outdated, consider fetching once to update it."
552+
echo "outdated"
562553
fi
563554
`, { env: { PATH: "$PATH:/data/adb/ap/bin:/data/adb/ksu/bin:/data/adb/magisk" }}).then((result) => {
564-
if (result.stdout.trim() !== '') appendToOutput(result.stdout.trim(), true);
555+
if (result.stdout.includes("outdated")) appendToOutput('[!] ' + translations.output_oudated_pif_prop, true);
565556
});
566557
}
567558

559+
// Notify when selinux is permissive
568560
function checkSeLinuxStatus() {
569561
exec('getenforce').then((result) => {
570562
if (result.errno !== 0) return;
571563
if (result.stdout.trim() === 'Permissive') {
572-
appendToOutput("[!] SELinux status is permissive.", true)
564+
appendToOutput("[!] " + translations.output_selinux_permissive, true)
573565
}
574566
});
575567
}
576568

569+
// Notify spoofSignature is on/off when rom is signed with releasekey/testkey
570+
function checkRomSignature() {
571+
const toggle = document.getElementById('spoofSignature');
572+
exec('unzip -l /system/etc/security/otacerts.zip | grep -oE "testkey|releasekey"').then((signature) => {
573+
if (signature.errno === 0) {
574+
if (signature.stdout.trim() === "testkey" && !toggle.selected) {
575+
appendToOutput('[!] ' + translations.output_testkey);
576+
} else if (signature.stdout.trim() === "releasekey" && toggle.selected) {
577+
appendToOutput('[+] ' + translations.output_releasekey);
578+
}
579+
}
580+
}).catch(() => {});
581+
}
582+
577583
function getDistance(touch1, touch2) {
578584
return Math.hypot(
579585
touch1.clientX - touch2.clientX,
@@ -638,6 +644,7 @@ document.querySelectorAll('md-dialog').forEach(dialog => {
638644
});
639645

640646
document.addEventListener('DOMContentLoaded', async () => {
647+
await loadTranslations();
641648
checkMMRL();
642649
appendSpoofConfigToggles();
643650
loadVersionFromModuleProp();
@@ -649,6 +656,7 @@ document.addEventListener('DOMContentLoaded', async () => {
649656
updateAutopif();
650657
checkSeLinuxStatus();
651658
checkPropDate();
659+
checkRomSignature();
652660

653661
document.querySelectorAll('[unresolved]').forEach(el => el.removeAttribute('unresolved'));
654662
});

0 commit comments

Comments
 (0)