Skip to content

Commit 6bc30a6

Browse files
Internationalize lab (#730)
Internationalize lab code. Use the HTML lang=... attribute to determine the locale (language) of the lab. This supports: * Built0-in text - includes a Japanese translation * Lab-specific text - you can use "text_LOCALE" which is preferred for the given LOCAE, otherwise use "text". Signed-off-by: David A. Wheeler <[email protected]>
1 parent 3d9412e commit 6bc30a6

File tree

1 file changed

+91
-13
lines changed

1 file changed

+91
-13
lines changed

docs/labs/checker.js

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,81 @@ let user_gave_up = false; // True if user ever gave up before user solved it
1717

1818
let startTime = Date.now();
1919

20+
// Current language
21+
let lang;
22+
23+
const resources = {
24+
en: {
25+
translation: {
26+
already_correct: 'The answer is already correct!',
27+
congrats: 'Congratulations! Your answer is correct!',
28+
congrats_all: 'Great work! All your answers are correct!',
29+
expecting: "We were expecting an answer like this:\n{0}",
30+
give_up_title: 'Give up and show an answer.',
31+
hint_title: 'Provide a hint given current attempt.',
32+
no_hints: 'Sorry, there are no hints for this lab.',
33+
no_matching_hint: 'Sorry, I cannot find a hint that matches your attempt.',
34+
reset_title: 'Reset initial state (throwing away current attempt).',
35+
try_harder: "Try harder! Don't give up so soon. Current time spent (in seconds): {0}",
36+
}
37+
},
38+
ja: {
39+
translation: {
40+
already_correct: '答えはすでに正しいです!',
41+
congrats: '「おめでとうございます!」あなたの答えは正解です!',
42+
congrats_all: '素晴らしい仕事でした!あなたの答えはすべて正解です!',
43+
expecting: "次のような答えを期待していました:\n{0}",
44+
give_up_title: '諦めて答えを示してください。',
45+
hint_title: '現在の試行に関するヒントを提供します。',
46+
no_hints: '申し訳ありませんが、このラボにはヒントがありません。',
47+
no_matching_hint: '申し訳ありませんが、あなたの試みに一致するヒントが見つかりません。',
48+
reset_title: '初期状態をリセットします (現在の試行を破棄します)。',
49+
try_harder: '「もっと頑張ってください! すぐに諦めないでください。現在の所要時間 (秒): {0}」',
50+
}
51+
},
52+
}
53+
54+
// Create a "format" method to simplify internationalization.
55+
// Use as: "Demo {0} result"".format(name);
56+
// https://www.geeksforgeeks.org/what-are-the-equivalent-of-printf-string-format-in-javascript/
57+
String.prototype.format = function () {
58+
const args = arguments;
59+
return this.replace(/{(\d+)}/g, function (match, number) {
60+
return typeof args[number] != 'undefined'
61+
? args[number]
62+
: match;
63+
});
64+
};
65+
66+
// Retrieve translation for given key from resources.
67+
function t(key) {
68+
let result = resources[lang]['translation'][key];
69+
70+
if (result === undefined) {
71+
result = resources['en']['translation'][key];
72+
}
73+
return result;
74+
}
75+
76+
// Retrieve translation from object for given field
77+
function retrieve_t(obj, field) {
78+
let result = obj[field + "_" + lang];
79+
80+
if (result === undefined) {
81+
result = obj[field];
82+
}
83+
return result;
84+
}
85+
86+
function determine_locale() {
87+
let lang = document.documentElement.lang;
88+
if (!lang) {
89+
lang = 'en';
90+
}
91+
return lang;
92+
}
93+
94+
2095
// This array contains the default pattern preprocessing commands, in order.
2196
// We process every pattern through these (in order) to create a final regex
2297
// to be used to match a pattern.
@@ -197,7 +272,7 @@ function calcOneMatch(attempt, index = 0, correct = correctRe) {
197272
*/
198273
function calcMatch(attempt, correct = correctRe) {
199274
if (!correct) { // Defensive test, should never happen.
200-
alert('Internal failure, correct value not defined or empty.');
275+
alert('Error: Internal failure, correct value not defined or empty.');
201276
return false;
202277
}
203278
for (let i = 0; i < correct.length; i++) {
@@ -344,9 +419,9 @@ function runCheck() {
344419
setTimeout(function() {
345420
let congrats_text;
346421
if (correctRe.length > 1) {
347-
congrats_text = 'Great work! All your answers are correct!';
422+
congrats_text = t('congrats');
348423
} else {
349-
congrats_text = 'Congratulations! Your answer is correct!';
424+
congrats_text = t('congrats_all');
350425
}
351426
alert(congrats_text);
352427
}, 100);
@@ -366,10 +441,10 @@ function findHint(attempt, validIndexes = undefined) {
366441
hint.presentRe.test(attempt[hint.index])) &&
367442
(!hint.absentRe ||
368443
!hint.absentRe.test(attempt[hint.index]))) {
369-
return hint.text;
444+
return retrieve_t(hint, 'text');
370445
}
371446
};
372-
return 'Sorry, I cannot find a hint that matches your attempt.';
447+
return t('no_matching_hint');
373448
}
374449

375450
/** Show a hint to the user. */
@@ -378,9 +453,9 @@ function showHint(e) {
378453
// alert(`Form id = ${e.target.form.id}`);
379454
let attempt = retrieveAttempt();
380455
if (calcMatch(attempt, correctRe)) {
381-
alert('The answer is already correct!');
456+
alert(t('already_correct'));
382457
} else if (!hints) {
383-
alert('Sorry, there are no hints for this lab.');
458+
alert(t('no_hints'));
384459
} else {
385460
let validIndexes = findIndexes(e.target.form);
386461
alert(findHint(attempt, validIndexes));
@@ -393,7 +468,7 @@ function showAnswer(e) {
393468
if (!user_solved) {
394469
user_gave_up = true;
395470
}
396-
alert(`We were expecting an answer like this:\n${goodAnswer}`);
471+
alert(t('expecting').format(goodAnswer));
397472
}
398473

399474
// "Give up" only shows the answer after this many seconds have elapsed.
@@ -403,7 +478,7 @@ function maybeShowAnswer(e) {
403478
let currentTime = Date.now();
404479
let elapsedTime = (currentTime - startTime) / 1000; // in seconds
405480
if (elapsedTime < MIN_DELAY_TIME) {
406-
alert("Try harder! Don't give up so soon. Current time spent (in seconds): " + elapsedTime);
481+
alert(t('try_harder').format(elapsedTime.toString()));
407482
} else {
408483
showAnswer(e);
409484
}
@@ -431,7 +506,7 @@ function processHints(requestedHints) {
431506

432507
// Hints must only contain these fields, since we ignore the rest.
433508
const allowedHintFields = new Set(
434-
['present', 'absent', 'text', 'examples', 'index',
509+
['present', 'absent', 'text', 'text_ja', 'examples', 'index',
435510
'preprocessing']);
436511

437512
// Process each hint
@@ -662,6 +737,9 @@ function initPage() {
662737
// Run a selftest on page load, to prevent later problems
663738
runSelftest();
664739

740+
// Set current locale
741+
lang = determine_locale();
742+
665743
// Set up user interaction for all attempts.
666744
let current = 0;
667745
while (true) {
@@ -673,19 +751,19 @@ function initPage() {
673751
for (let hintButton of document.querySelectorAll("button.hintButton")) {
674752
hintButton.addEventListener('click', (e) => { showHint(e); });
675753
if (!hintButton.title) {
676-
hintButton.title = 'Provide a hint given current attempt.';
754+
hintButton.title = t('hint_title');
677755
}
678756
}
679757
for (let resetButton of document.querySelectorAll("button.resetButton")) {
680758
resetButton.addEventListener('click', (e) => { resetForm(e); });
681759
if (!resetButton.title) {
682-
resetButton.title = 'Reset initial state (throwing away current attempt).';
760+
resetButton.title = t('reset_title');
683761
}
684762
}
685763
for (let giveUpButton of document.querySelectorAll("button.giveUpButton")) {
686764
giveUpButton.addEventListener('click', (e) => { maybeShowAnswer(e); });
687765
if (!giveUpButton.title) {
688-
giveUpButton.title = 'Give up and show an answer.';
766+
giveUpButton.title = t('give_up_title');
689767
}
690768
}
691769
if (info.debug) {

0 commit comments

Comments
 (0)