Skip to content

Commit

Permalink
Refactor format() so it's not a global String property
Browse files Browse the repository at this point in the history
While JavaScript *allows* global properties on Strings to
be added, it's not necessarily a good idea, as those are global
(and could interfere with other things).

To reduce the risk of future problems, use a created function instead,
to avoid conflicts.

This also adds a myAssert and improves internal comments about
indexing as related changes.

Signed-off-by: David A. Wheeler <dwheeler@dwheeler.com>
  • Loading branch information
david-a-wheeler committed Feb 6, 2025
1 parent 133f2b7 commit 63b0d95
Showing 1 changed file with 35 additions and 10 deletions.
45 changes: 35 additions & 10 deletions docs/labs/checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,35 @@ const resources = {
},
};

// Create a "format" method to simplify internationalization.
// Use as: "Demo {0} result"".format(name);
/** Provide an "assert" (JavaScript doesn't have one built-in).
* This one uses "Error" to provide a stack trace.
*/
function myAssert(condition, message) {
if (!condition) {
throw new Error(message || "Assertion failed");
}
}

// Format a string, replacing {NUM} with item NUM.
// We use this function to simplify internationalization.
// Use as: myFormat("Demo {0} result", ["Name"])
// https://www.geeksforgeeks.org/what-are-the-equivalent-of-printf-string-format-in-javascript/
String.prototype.format = function () {
const args = arguments;
return this.replace(/{(\d+)}/g, function (match, number) {
return typeof args[number] != 'undefined'
? args[number]
// This is *not* set as a property on String; if we did that,
// we'd modify the global namespace, possibly messing up something
// already there.
function myFormat(s, replacements) {
return s.replace(/{(\d+)}/g, function (match, number) {
return typeof replacements[number] != 'undefined'
? replacements[number]
: match;
});
};

// Run some built-in tests on startup, to ensure all is okay.
myAssert(myFormat("Hello", []) === "Hello");
myAssert(myFormat("Hello {0}, are you {1}?", ["friend", "well"]) ===
"Hello friend, are you well?");

// Retrieve translation for given key from resources.
function t(key) {
let result = resources[lang]['translation'][key];
Expand Down Expand Up @@ -370,15 +387,23 @@ const attemptIdPattern = /^attempt(\d+)$/;

/*
* Given Node @form in document, return array of indexes of input/textareas
* that are relevant for that form.
* The values retrieved are *input* field indexes (`inputIndexes`),
* starting at 0 for the first user input.
*
* When there's only one form, this is simply the array of all valid indexes.
* However, a page can have multiple forms; in that case this returns
* only the indexes valid for this specific form.
*
* Note: At one time we ran this calculation when a user pressed
* a button. However, if you *translate* the page using Chrome's translator,
* that will cause this routine to fail because querySelectorAll will fail.
* To work around this,
* it's better to calculate all of these values on page load and store it
* (e.g., as dataset.inputIndexes values on the buttons).
* If you run this early, users can use the web browser's built-in translator,
* see the translated HTML, and have the lab work (though the lab
* responses won't be translated).
*/
function findIndexes(form) {
try {
Expand Down Expand Up @@ -556,7 +581,7 @@ function showAnswer(e) {
if (!user_solved) {
user_gave_up = true;
}
alert(t('expecting').format(goodAnswer));
alert(myFormat(t('expecting'), [goodAnswer]));
}

// "Give up" only shows the answer after this many seconds have elapsed
Expand Down Expand Up @@ -594,7 +619,7 @@ function elapsedTimeSinceClue() {
function maybeShowAnswer(e) {
let elapsedTime = elapsedTimeSinceClue();
if (elapsedTime < GIVE_UP_DELAY_TIME) {
alert(t('try_harder_give_up').format(elapsedTime.toString()));
alert(myFormat(t('try_harder_give_up'), [elapsedTime.toString()]));
} else {
showAnswer(e);
}
Expand All @@ -607,7 +632,7 @@ function maybeShowHint(e) {
// people can re-see a previously-seen hint as long as they
// have not changed anything since seeing the hint.
if (changedInputSinceHint && (elapsedTime < HINT_DELAY_TIME)) {
alert(t('try_harder_hint').format(HINT_DELAY_TIME.toString()));
alert(myFormat(t('try_harder_hint'), [HINT_DELAY_TIME.toString()]));
} else {
lastHintTime = Date.now(); // Set new delay time start
changedInputSinceHint = false; // Allow redisplay of hint
Expand Down

0 comments on commit 63b0d95

Please sign in to comment.