Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve i18n utility, add English fallbacks #186

Merged
merged 8 commits into from Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
69 changes: 40 additions & 29 deletions scripts/global.js
@@ -1,6 +1,6 @@
import { Mempool } from './mempool.js';
import Masternode from './masternode.js';
import { ALERTS, start as i18nStart, translation } from './i18n.js';
import { ALERTS, tr, start as i18nStart, translation } from './i18n.js';
import * as jdenticon from 'jdenticon';
import {
masterKey,
Expand All @@ -10,7 +10,7 @@
decryptWallet,
getNewAddress,
getDerivationPath,
LegacyMasterKey,

Check warning on line 13 in scripts/global.js

View workflow job for this annotation

GitHub Actions / ESLint

scripts/global.js#L13

'LegacyMasterKey' is defined but never used (@typescript-eslint/no-unused-vars)
} from './wallet.js';
import { getNetwork, HistoricalTxType } from './network.js';
import {
Expand Down Expand Up @@ -733,7 +733,6 @@
`"${sanitizeHTML(
cScan.data.substring(0, Math.min(cScan.data.length, 6))
)}…" ${ALERTS.QR_SCANNER_BAD_RECEIVER}`,
[],
7500
);
}
Expand Down Expand Up @@ -1015,7 +1014,17 @@
domLoadMore.style.display = 'none';
}

// Render the new Activity lists
renderActivityGUI(arrTXs);
}

/**
* Renders the Activity GUIs (without syncing or refreshing)
* @param {Array<import('./network.js').HistoricalTx>} arrTXs
*/
export async function renderActivityGUI(arrTXs) {
// For Staking: Filter the list for only Stakes, display total rewards from known history
const cNet = getNetwork();
const arrStakes = arrTXs.filter((a) => a.type === HistoricalTxType.STAKE);
const nRewards = arrStakes.reduce((a, b) => a + b.amount, 0);
doms.domStakingRewardsTitle.innerHTML = `${
Expand Down Expand Up @@ -1361,8 +1370,10 @@
const ticker = cChainParams.current.TICKER;
createAlert(
'warning',
ALERTS.MN_NOT_ENOUGH_COLLAT,
[{ amount }, { ticker }],
tr(ALERTS.MN_NOT_ENOUGH_COLLAT, [
{ amount: amount },
{ ticker: ticker },
]),
10000
);
} else {
Expand All @@ -1372,8 +1383,10 @@
const ticker = cChainParams.current.TICKER;
createAlert(
'warning',
ALERTS.MN_ENOUGH_BUT_NO_COLLAT,
[{ amount }, { ticker }],
tr(ALERTS.MN_ENOUGH_BUT_NO_COLLAT, [
{ amount },
{ ticker },
]),
10000
);
}
Expand Down Expand Up @@ -1473,7 +1486,7 @@
if (!(await hasEncryptedWallet()) && fEncrypted) {
const strDecWIF = await decrypt(strPrivKey, strPassword);
if (!strDecWIF || strDecWIF === 'decryption failed!') {
return createAlert('warning', ALERTS.FAILED_TO_IMPORT, [], 6000);
return createAlert('warning', ALERTS.FAILED_TO_IMPORT, 6000);
} else {
await importWallet({
newWif: strDecWIF,
Expand Down Expand Up @@ -1503,25 +1516,21 @@
export async function guiEncryptWallet() {
// Disable wallet encryption in testnet mode
if (cChainParams.current.isTestnet)
return createAlert(
'warning',
ALERTS.TESTNET_ENCRYPTION_DISABLED,
[],
2500
);
return createAlert('warning', ALERTS.TESTNET_ENCRYPTION_DISABLED, 2500);

// Fetch our inputs, ensure they're of decent entropy + match eachother
const strPass = doms.domEncryptPasswordFirst.value,
strPassRetype = doms.domEncryptPasswordSecond.value;
if (strPass.length < MIN_PASS_LENGTH)
return createAlert(
'warning',
ALERTS.PASSWORD_TOO_SMALL,
[{ MIN_PASS_LENGTH: MIN_PASS_LENGTH }],
tr(ALERTS.PASSWORD_TOO_SMALL, [
{ MIN_PASS_LENGTH: MIN_PASS_LENGTH },
]),
4000
);
if (strPass !== strPassRetype)
return createAlert('warning', ALERTS.PASSWORD_DOESNT_MATCH, [], 2250);
return createAlert('warning', ALERTS.PASSWORD_DOESNT_MATCH, 2250);

// If this wallet is already encrypted, then we'll check for the current password and ensure it decrypts properly too
if (await hasEncryptedWallet()) {
Expand All @@ -1535,7 +1544,7 @@

// Encrypt the wallet using the new password
await encryptWallet(strPass);
createAlert('success', ALERTS.NEW_PASSWORD_SUCCESS, [], 5500);
createAlert('success', ALERTS.NEW_PASSWORD_SUCCESS, 5500);

// Hide and reset the encryption modal
$('#encryptWalletModal').modal('hide');
Expand Down Expand Up @@ -1599,8 +1608,7 @@
e.returnValue = false;
return createAlert(
'warning',
ALERTS.UNSUPPORTED_CHARACTER,
[{ char: char }],
tr(ALERTS.UNSUPPORTED_CHARACTER, [{ char: char }]),
3500
);
}
Expand All @@ -1624,7 +1632,7 @@
export async function generateVanityWallet() {
if (isVanityGenerating) return stopSearch();
if (typeof Worker === 'undefined')
return createAlert('error', ALERTS.UNSUPPORTED_WEBWORKERS, [], 7500);
return createAlert('error', ALERTS.UNSUPPORTED_WEBWORKERS, 7500);
// Generate a vanity address with the given prefix
if (
doms.domPrefix.value.length === 0 ||
Expand All @@ -1649,16 +1657,14 @@
if (!MAP_B58.toLowerCase().includes(char.toLowerCase()))
return createAlert(
'warning',
ALERTS.UNSUPPORTED_CHARACTER,
[{ char: char }],
tr(ALERTS.UNSUPPORTED_CHARACTER, [{ char: char }]),
3500
);
// We also don't want users to be mining addresses for years... so cap the letters to four until the generator is more optimized
if (doms.domPrefix.value.length > 5)
return createAlert(
'warning',
ALERTS.UNSUPPORTED_CHARACTER,
[{ char: char }],
tr(ALERTS.UNSUPPORTED_CHARACTER, [{ char: char }]),
3500
);
}
Expand Down Expand Up @@ -1794,10 +1800,10 @@
strColdAddress.length === 34
) {
await setColdStakingAddress(strColdAddress);
createAlert('info', ALERTS.STAKE_ADDR_SET, [], 5000);
createAlert('info', ALERTS.STAKE_ADDR_SET, 5000);
return true;
} else {
createAlert('warning', ALERTS.STAKE_ADDR_BAD, [], 2500);
createAlert('warning', ALERTS.STAKE_ADDR_BAD, 2500);
return false;
}
} else {
Expand Down Expand Up @@ -2632,7 +2638,12 @@
'{state}',
state
);
if (fAlert) createAlert('warning', ALERTS.MN_STATE, [{ state }], 6000);
if (fAlert)
createAlert(
'warning',
tr(ALERTS.MN_STATE, [{ state: state }]),
6000
);
} else {
// connection problem
doms.domMnTextErrors.innerHTML = ALERTS.MN_CANT_CONNECT;
Expand Down Expand Up @@ -2711,7 +2722,7 @@
const localProposals = account?.localProposals || [];
localProposals.push(proposal);
await database.addAccount({ localProposals });
createAlert('success', translation.PROPOSAL_CREATED, [], 7500);
createAlert('success', translation.PROPOSAL_CREATED, 7500);
updateGovernanceTab();
}
}
Expand Down Expand Up @@ -2743,7 +2754,7 @@
evt.preventDefault();
// Disable Save your wallet warning on unload
if (!cChainParams.current.isTestnet)
createAlert('warning', ALERTS.SAVE_WALLET_PLEASE, [], 10000);
createAlert('warning', ALERTS.SAVE_WALLET_PLEASE, 10000);
// Most browsers ignore this nowadays, but still, keep it 'just incase'
return (evt.returnValue = translation.BACKUP_OR_ENCRYPT_WALLET);
};
Expand Down
41 changes: 32 additions & 9 deletions scripts/i18n.js
Expand Up @@ -7,6 +7,9 @@ import { fr_translation } from '../locale/fr/translation.js';
import { de_translation } from '../locale/de/translation.js';
import { Database } from './database.js';
import { fillAnalyticSelect, setTranslation } from './settings.js';
import { renderActivityGUI, updateEncryptionGUI } from './global.js';
import { masterKey } from './wallet.js';
import { getNetwork } from './network.js';

export const ALERTS = {};
export let translation = {};
Expand All @@ -29,8 +32,28 @@ export const translatableLanguages = {
*/
export function switchTranslation(langName) {
if (arrActiveLangs.find((lang) => lang.code === langName)) {
translation = translatableLanguages[langName];
translate(translation);
// Load every 'active' key of the language, otherwise, we'll default the key to the EN file
const arrNewLang = translatableLanguages[langName];
for (const strKey of Object.keys(arrNewLang)) {
// Skip empty and/or missing i18n keys, defaulting them to EN
if (!arrNewLang[strKey]) {
translation[strKey] = translatableLanguages.en[strKey];
continue;
}

// Apply the new i18n value to our runtime i18n sheet
translation[strKey] = arrNewLang[strKey];
}

// Translate static`data-i18n` tags
translateStaticHTML(translation);

// Translate any dynamic elements necessary
const cNet = getNetwork();
if (masterKey && cNet) {
updateEncryptionGUI();
renderActivityGUI(cNet.arrTxHistory);
}
loadAlerts();
fillAnalyticSelect();
return true;
Expand All @@ -46,16 +69,16 @@ export function switchTranslation(langName) {
}

/**
* Takes a string that includes {x} and replaces that based on what is in the array of objects
* Takes an i18n string that includes `{x}` and replaces that based on what is in the array of objects
* @param {string} message
* @param {array<Object>} variables
* @param {Array<Object>} variables
* @returns a string with the variables implemented in the string
*
* @example
* //returns "test this"
* translateAlerts("test {x}" [x : "this"])
* tr("test {x}" [x: "this"])
*/
export function translateAlerts(message, variables) {
export function tr(message, variables) {
variables.forEach((element) => {
message = message.replaceAll(
'{' + Object.keys(element)[0] + '}',
Expand All @@ -66,11 +89,11 @@ export function translateAlerts(message, variables) {
}

/**
* Translates all the static html based on the tag data-i18n
* Translates all static HTML based on the `data-i18n` tag
* @param {Array} i18nLangs
*
*/
export function translate(i18nLangs) {
export function translateStaticHTML(i18nLangs) {
if (!i18nLangs) return;

document.querySelectorAll('[data-i18n]').forEach(function (element) {
Expand Down Expand Up @@ -161,5 +184,5 @@ export async function start() {
setTranslation('en');
}
}
translate(translation);
translateStaticHTML(translation);
}
27 changes: 12 additions & 15 deletions scripts/misc.js
@@ -1,4 +1,4 @@
import { translateAlerts, translation } from './i18n.js';
import { translation } from './i18n.js';
import { doms } from './global.js';
import qrcode from 'qrcode-generator';
import bs58 from 'bs58';
Expand Down Expand Up @@ -82,10 +82,16 @@ export function downloadBlob(content, filename, contentType) {
pom.click();
}

/* --- NOTIFICATIONS --- */
// Alert - Do NOT display arbitrary / external errors, the use of `.innerHTML` allows for input styling at this cost.
// Supported types: success, info, warning
export function createAlert(type, message, alertVariables = [], timeout = 0) {
/**
* Create a custom GUI Alert popup
*
* ### Do NOT display arbitrary / external errors:
* - The use of `.innerHTML` allows for input styling at this cost.
* @param {'success'|'info'|'warning'} type - The styling type of the alert
* @param {string} message - The message to relay to the user
* @param {number?} timeout - The time in `ms` until the alert expires (Defaults to never expiring)
*/
export function createAlert(type, message, timeout = 0) {
const domAlert = document.createElement('div');
domAlert.classList.add('notifyWrapper');
domAlert.classList.add(type);
Expand All @@ -96,15 +102,6 @@ export function createAlert(type, message, alertVariables = [], timeout = 0) {
domAlert.classList.add('bounce');
}, 100);

// Maintainer QoL adjustment: if `alertVariables` is a number, it is instead assumed to be `timeout`
if (typeof alertVariables === 'number') {
timeout = alertVariables;
alertVariables = [];
}

// Apply translations
const translatedMessage = translateAlerts(message, alertVariables);

// Colors for types
let typeIcon;
switch (type) {
Expand All @@ -127,7 +124,7 @@ export function createAlert(type, message, alertVariables = [], timeout = 0) {
<i class="fas ${typeIcon} fa-xl"></i>
</div>
<div class="notifyText">
${translatedMessage}
${message}
</div>`;
domAlert.destroy = () => {
// Fully destroy timers + DOM elements, no memory leaks!
Expand Down
4 changes: 2 additions & 2 deletions scripts/network.js
Expand Up @@ -168,7 +168,7 @@ export class ExplorerNetwork extends Network {
error() {
if (this.enabled) {
this.disable();
createAlert('warning', ALERTS.CONNECTION_FAILED, []);
createAlert('warning', ALERTS.CONNECTION_FAILED);
}
}

Expand Down Expand Up @@ -614,7 +614,7 @@ export function setNetwork(network) {
}

/**
* Sets the network in use by MPW.
* Gets the network in use by MPW.
* @returns {ExplorerNetwork?} Returns the network in use, may be null if MPW hasn't properly loaded yet.
*/
export function getNetwork() {
Expand Down
23 changes: 14 additions & 9 deletions scripts/promos.js
Expand Up @@ -7,7 +7,7 @@ import {
downloadBlob,
getAlphaNumericRand,
} from './misc';
import { ALERTS, translation } from './i18n';
import { ALERTS, translation, tr } from './i18n';
import { getNetwork } from './network';
import { scanQRCode } from './scanner';
import { createAndSendTransaction } from './transactions';
Expand Down Expand Up @@ -224,18 +224,22 @@ export async function createPromoCode(strCode, nAmount, fAddRandomness = true) {
// Ensure the amount is sane
const min = 0.01;
if (nAmount < min) {
return createAlert('warning', ALERTS.PROMO_MIN, [
{ min },
{ ticker: cChainParams.current.TICKER },
]);
return createAlert(
'warning',
tr(ALERTS.PROMO_MIN, [
{ min },
{ ticker: cChainParams.current.TICKER },
])
);
}

// Ensure there's no more than half the device's cores used
if (arrPromoCreationThreads.length >= navigator.hardwareConcurrency)
return createAlert(
'warning',
ALERTS.PROMO_MAX_QUANTITY,
[{ quantity: navigator.hardwareConcurrency }],
tr(ALERTS.PROMO_MAX_QUANTITY, [
{ quantity: navigator.hardwareConcurrency },
]),
4000
);

Expand All @@ -247,8 +251,9 @@ export async function createPromoCode(strCode, nAmount, fAddRandomness = true) {
if (getBalance() - nReservedBalance < nAmount * COIN + PROMO_FEE * 2) {
return createAlert(
'warning',
ALERTS.PROMO_NOT_ENOUGH,
[{ ticker: cChainParams.current.TICKER }],
tr(ALERTS.PROMO_NOT_ENOUGH, [
{ ticker: cChainParams.current.TICKER },
]),
4000
);
}
Expand Down