Skip to content

Commit 916cadc

Browse files
Kamesutadscalzi
authored andcommitted
Localize HeliosLauncher UI using lang files (dscalzi#301)
* First step to use Language .json file in ejs * i18n for landing.ejs * i18n for login.ejs * i18n for loginOptions.ejs * i18n for overlay.ejs * i18n for settings.ejs * i18n for waiting.ejs * i18n for welcome.ejs * langloader.js placeholder support * i18n for landing.js * i18n for login.js * i18n for overlay.js * i18n for settings.js * i18n for uibinder.js * i18n for uicore.js * remove html language replacement * use toml for i18n * Fix mojang/microsoft status icon is undefined * cascadable langloader * separate lang file for customization * move some placeholder text to _placeholder.toml * Update * Reduce package lock diff. * Remove another placeholder. * Checkbox does not require translation. * Icons don't need translation. * Leave placeholders inline. * Fix translation for news pages. * Remove more unneeded translations. --------- Co-authored-by: Daniel Scalzi <[email protected]>
1 parent 15615cf commit 916cadc

23 files changed

+580
-302
lines changed

app/app.ejs

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
22
<head>
33
<meta charset="utf-8" http-equiv="Content-Security-Policy" content="script-src 'self' 'sha256-In6B8teKZQll5heMl9bS7CESTbGvuAt3VVV86BUQBDk='"/>
4-
<title>Stargate Atohara</title>
4+
<title><%= lang('app.title') %></title>
55
<script src="./assets/js/scripts/uicore.js"></script>
66
<script src="./assets/js/scripts/uibinder.js"></script>
77
<link type="text/css" rel="stylesheet" href="./assets/css/launcher.css">
@@ -61,11 +61,5 @@
6161
</div>
6262
</div>
6363
</div>
64-
<script>
65-
// Load language
66-
for(let key of Object.keys(Lang.query('html'))){
67-
document.getElementById(key).innerHTML = Lang.query(`html.${key}`)
68-
}
69-
</script>
7064
</body>
7165
</html>

app/assets/js/langloader.js

+27-5
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,43 @@
11
const fs = require('fs-extra')
22
const path = require('path')
3+
const toml = require('toml')
4+
const merge = require('lodash.merge')
35

46
let lang
57

68
exports.loadLanguage = function(id){
7-
lang = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'lang', `${id}.json`))) || {}
9+
lang = merge(lang || {}, toml.parse(fs.readFileSync(path.join(__dirname, '..', 'lang', `${id}.toml`))) || {})
810
}
911

10-
exports.query = function(id){
12+
exports.query = function(id, placeHolders){
1113
let query = id.split('.')
1214
let res = lang
1315
for(let q of query){
1416
res = res[q]
1517
}
16-
return res === lang ? {} : res
18+
let text = res === lang ? '' : res
19+
if (placeHolders) {
20+
Object.entries(placeHolders).forEach(([key, value]) => {
21+
text = text.replace(`{${key}}`, value)
22+
})
23+
}
24+
return text
25+
}
26+
27+
exports.queryJS = function(id, placeHolders){
28+
return exports.query(`js.${id}`, placeHolders)
1729
}
1830

19-
exports.queryJS = function(id){
20-
return exports.query(`js.${id}`)
31+
exports.queryEJS = function(id, placeHolders){
32+
return exports.query(`ejs.${id}`, placeHolders)
33+
}
34+
35+
exports.setupLanguage = function(){
36+
// Load Language Files
37+
exports.loadLanguage('en_US')
38+
// Uncomment this when translations are ready
39+
//exports.loadLanguage('xx_XX')
40+
41+
// Load Custom Language File for Launcher Customizer
42+
exports.loadLanguage('_custom')
2143
}

app/assets/js/preloader.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ DistroAPI['commonDir'] = ConfigManager.getCommonDirectory()
2323
DistroAPI['instanceDir'] = ConfigManager.getInstanceDirectory()
2424

2525
// Load Strings
26-
LangLoader.loadLanguage('en_US')
26+
LangLoader.setupLanguage()
2727

2828
/**
2929
*

app/assets/js/scripts/landing.js

+45-47
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ document.getElementById('launch_button').addEventListener('click', async e => {
125125
}
126126
} catch(err) {
127127
loggerLanding.error('Unhandled error in during launch process.', err)
128-
showLaunchFailure('Error During Launch', 'See console (CTRL + Shift + i) for more details.')
128+
showLaunchFailure(Lang.queryJS('landing.launch.failureTitle'), Lang.queryJS('landing.launch.failureText'))
129129
}
130130
})
131131

@@ -145,7 +145,7 @@ document.getElementById('avatarOverlay').onclick = async e => {
145145

146146
// Bind selected account
147147
function updateSelectedAccount(authUser){
148-
let username = 'No Account Selected'
148+
let username = Lang.queryJS('landing.selectedAccount.noAccountSelected')
149149
if(authUser != null){
150150
if(authUser.displayName != null){
151151
username = authUser.displayName
@@ -165,14 +165,14 @@ function updateSelectedServer(serv){
165165
}
166166
ConfigManager.setSelectedServer(serv != null ? serv.rawServer.id : null)
167167
ConfigManager.save()
168-
server_selection_button.innerHTML = '\u2022 ' + (serv != null ? serv.rawServer.name : 'No Server Selected')
168+
server_selection_button.innerHTML = '&#8226; ' + (serv != null ? serv.rawServer.name : Lang.queryJS('landing.noSelection'))
169169
if(getCurrentView() === VIEWS.settings){
170170
animateSettingsTabRefresh()
171171
}
172172
setLaunchEnabled(serv != null)
173173
}
174174
// Real text is set in uibinder.js on distributionIndexDone.
175-
server_selection_button.innerHTML = '\u2022 Loading..'
175+
server_selection_button.innerHTML = '&#8226; ' + Lang.queryJS('landing.selectedServer.loading')
176176
server_selection_button.onclick = async e => {
177177
e.target.blur()
178178
await toggleServerSelection(true)
@@ -201,16 +201,14 @@ const refreshMojangStatuses = async function(){
201201
for(let i=0; i<statuses.length; i++){
202202
const service = statuses[i]
203203

204+
const tooltipHTML = `<div class="mojangStatusContainer">
205+
<span class="mojangStatusIcon" style="color: ${MojangRestAPI.statusToHex(service.status)};">&#8226;</span>
206+
<span class="mojangStatusName">${service.name}</span>
207+
</div>`
204208
if(service.essential){
205-
tooltipEssentialHTML += `<div class="mojangStatusContainer">
206-
<span class="mojangStatusIcon" style="color: ${MojangRestAPI.statusToHex(service.status)};">&#8226;</span>
207-
<span class="mojangStatusName">${service.name}</span>
208-
</div>`
209+
tooltipEssentialHTML += tooltipHTML
209210
} else {
210-
tooltipNonEssentialHTML += `<div class="mojangStatusContainer">
211-
<span class="mojangStatusIcon" style="color: ${MojangRestAPI.statusToHex(service.status)};">&#8226;</span>
212-
<span class="mojangStatusName">${service.name}</span>
213-
</div>`
211+
tooltipNonEssentialHTML += tooltipHTML
214212
}
215213

216214
if(service.status === 'yellow' && status !== 'red'){
@@ -243,14 +241,14 @@ const refreshServerStatus = async (fade = false) => {
243241
loggerLanding.info('Refreshing Server Status')
244242
const serv = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer())
245243

246-
let pLabel = 'SERVER'
247-
let pVal = 'OFFLINE'
244+
let pLabel = Lang.queryJS('landing.serverStatus.server')
245+
let pVal = Lang.queryJS('landing.serverStatus.offline')
248246

249247
try {
250248

251249
const servStat = await getServerStatus(47, serv.hostname, serv.port)
252250
console.log(servStat)
253-
pLabel = 'PLAYERS'
251+
pLabel = Lang.queryJS('landing.serverStatus.players')
254252
pVal = servStat.players.online + '/' + servStat.players.max
255253

256254
} catch (err) {
@@ -288,7 +286,7 @@ function showLaunchFailure(title, desc){
288286
setOverlayContent(
289287
title,
290288
desc,
291-
'Okay'
289+
Lang.queryJS('landing.launch.okay')
292290
)
293291
setOverlayHandler(null)
294292
toggleOverlay(true)
@@ -304,7 +302,7 @@ function showLaunchFailure(title, desc){
304302
*/
305303
async function asyncSystemScan(effectiveJavaOptions, launchAfter = true){
306304

307-
setLaunchDetails('Checking system info..')
305+
setLaunchDetails(Lang.queryJS('landing.systemScan.checking'))
308306
toggleLaunchArea(true)
309307
setLaunchPercentage(0, 100)
310308

@@ -317,30 +315,30 @@ async function asyncSystemScan(effectiveJavaOptions, launchAfter = true){
317315
// If the result is null, no valid Java installation was found.
318316
// Show this information to the user.
319317
setOverlayContent(
320-
'No Compatible<br>Java Installation Found',
321-
`In order to join WesterosCraft, you need a 64-bit installation of Java ${effectiveJavaOptions.suggestedMajor}. Would you like us to install a copy?`,
322-
'Install Java',
323-
'Install Manually'
318+
Lang.queryJS('landing.systemScan.noCompatibleJava'),
319+
Lang.queryJS('landing.systemScan.installJavaMessage', { 'major': effectiveJavaOptions.suggestedMajor }),
320+
Lang.queryJS('landing.systemScan.installJava'),
321+
Lang.queryJS('landing.systemScan.installJavaManually')
324322
)
325323
setOverlayHandler(() => {
326-
setLaunchDetails('Preparing Java Download..')
324+
setLaunchDetails(Lang.queryJS('landing.systemScan.javaDownloadPrepare'))
327325
toggleOverlay(false)
328326

329327
try {
330328
downloadJava(effectiveJavaOptions, launchAfter)
331329
} catch(err) {
332330
loggerLanding.error('Unhandled error in Java Download', err)
333-
showLaunchFailure('Error During Java Download', 'See console (CTRL + Shift + i) for more details.')
331+
showLaunchFailure(Lang.queryJS('landing.systemScan.javaDownloadFailureTitle'), Lang.queryJS('landing.systemScan.javaDownloadFailureText'))
334332
}
335333
})
336334
setDismissHandler(() => {
337335
$('#overlayContent').fadeOut(250, () => {
338336
//$('#overlayDismiss').toggle(false)
339337
setOverlayContent(
340-
'Java is Required<br>to Launch',
341-
`A valid x64 installation of Java ${effectiveJavaOptions.suggestedMajor} is required to launch.<br><br>Please refer to our <a href="https://github.com/dscalzi/HeliosLauncher/wiki/Java-Management#manually-installing-a-valid-version-of-java">Java Management Guide</a> for instructions on how to manually install Java.`,
342-
'I Understand',
343-
'Go Back'
338+
Lang.queryJS('landing.systemScan.javaRequired', { 'major': effectiveJavaOptions.suggestedMajor }),
339+
Lang.queryJS('landing.systemScan.javaRequiredMessage', { 'major': effectiveJavaOptions.suggestedMajor }),
340+
Lang.queryJS('landing.systemScan.javaRequiredDismiss'),
341+
Lang.queryJS('landing.systemScan.javaRequiredCancel')
344342
)
345343
setOverlayHandler(() => {
346344
toggleLaunchArea(false)
@@ -385,7 +383,7 @@ async function downloadJava(effectiveJavaOptions, launchAfter = true) {
385383
effectiveJavaOptions.distribution)
386384

387385
if(asset == null) {
388-
throw new Error('Failed to find OpenJDK distribution.')
386+
throw new Error(Lang.queryJS('landing.downloadJava.findJdkFailure'))
389387
}
390388

391389
let received = 0
@@ -400,7 +398,7 @@ async function downloadJava(effectiveJavaOptions, launchAfter = true) {
400398
if(!await validateLocalFile(asset.path, asset.algo, asset.hash)) {
401399
log.error(`Hashes do not match, ${asset.id} may be corrupted.`)
402400
// Don't know how this could happen, but report it.
403-
throw new Error('Downloaded JDK has bad hash, file may be corrupted.')
401+
throw new Error(Lang.queryJS('landing.downloadJava.javaDownloadCorruptedError'))
404402
}
405403
}
406404

@@ -409,7 +407,7 @@ async function downloadJava(effectiveJavaOptions, launchAfter = true) {
409407
remote.getCurrentWindow().setProgressBar(2)
410408

411409
// Wait for extration to complete.
412-
const eLStr = 'Extracting Java'
410+
const eLStr = Lang.queryJS('landing.downloadJava.extractingJava')
413411
let dotStr = ''
414412
setLaunchDetails(eLStr)
415413
const extractListener = setInterval(() => {
@@ -431,7 +429,7 @@ async function downloadJava(effectiveJavaOptions, launchAfter = true) {
431429
ConfigManager.save()
432430

433431
clearInterval(extractListener)
434-
setLaunchDetails('Java Installed!')
432+
setLaunchDetails(Lang.queryJS('landing.downloadJava.javaInstalled'))
435433

436434
// TODO Callback hell
437435
// Refactor the launch functions
@@ -456,7 +454,7 @@ async function dlAsync(login = true) {
456454

457455
const loggerLaunchSuite = LoggerUtil.getLogger('LaunchSuite')
458456

459-
setLaunchDetails('Loading server information..')
457+
setLaunchDetails(Lang.queryJS('landing.dlAsync.loadingServerInfo'))
460458

461459
let distro
462460

@@ -465,7 +463,7 @@ async function dlAsync(login = true) {
465463
onDistroRefresh(distro)
466464
} catch(err) {
467465
loggerLaunchSuite.error('Unable to refresh distribution index.', err)
468-
showLaunchFailure('Fatal Error', 'Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details.')
466+
showLaunchFailure(Lang.queryJS('landing.dlAsync.fatalError'), Lang.queryJS('landing.dlAsync.unableToLoadDistributionIndex'))
469467
return
470468
}
471469

@@ -478,7 +476,7 @@ async function dlAsync(login = true) {
478476
}
479477
}
480478

481-
setLaunchDetails('Please wait..')
479+
setLaunchDetails(Lang.queryJS('landing.dlAsync.pleaseWait'))
482480
toggleLaunchArea(true)
483481
setLaunchPercentage(0, 100)
484482

@@ -494,17 +492,17 @@ async function dlAsync(login = true) {
494492

495493
fullRepairModule.childProcess.on('error', (err) => {
496494
loggerLaunchSuite.error('Error during launch', err)
497-
showLaunchFailure('Error During Launch', err.message || 'See console (CTRL + Shift + i) for more details.')
495+
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), err.message || Lang.queryJS('landing.dlAsync.errorDuringLaunchText'))
498496
})
499497
fullRepairModule.childProcess.on('close', (code, _signal) => {
500498
if(code !== 0){
501499
loggerLaunchSuite.error(`Full Repair Module exited with code ${code}, assuming error.`)
502-
showLaunchFailure('Error During Launch', 'See console (CTRL + Shift + i) for more details.')
500+
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
503501
}
504502
})
505503

506504
loggerLaunchSuite.info('Validating files.')
507-
setLaunchDetails('Validating file integrity..')
505+
setLaunchDetails(Lang.queryJS('landing.dlAsync.validatingFileIntegrity'))
508506
let invalidFileCount = 0
509507
try {
510508
invalidFileCount = await fullRepairModule.verifyFiles(percent => {
@@ -513,14 +511,14 @@ async function dlAsync(login = true) {
513511
setLaunchPercentage(100)
514512
} catch (err) {
515513
loggerLaunchSuite.error('Error during file validation.')
516-
showLaunchFailure('Error During File Verification', err.displayable || 'See console (CTRL + Shift + i) for more details.')
514+
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringFileVerificationTitle'), err.displayable || Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
517515
return
518516
}
519517

520518

521519
if(invalidFileCount > 0) {
522520
loggerLaunchSuite.info('Downloading files.')
523-
setLaunchDetails('Downloading files..')
521+
setLaunchDetails(Lang.queryJS('landing.dlAsync.downloadingFiles'))
524522
setLaunchPercentage(0)
525523
try {
526524
await fullRepairModule.download(percent => {
@@ -529,7 +527,7 @@ async function dlAsync(login = true) {
529527
setDownloadPercentage(100)
530528
} catch(err) {
531529
loggerLaunchSuite.error('Error during file download.')
532-
showLaunchFailure('Error During File Download', err.displayable || 'See console (CTRL + Shift + i) for more details.')
530+
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringFileDownloadTitle'), err.displayable || Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
533531
return
534532
}
535533
} else {
@@ -541,7 +539,7 @@ async function dlAsync(login = true) {
541539

542540
fullRepairModule.destroyReceiver()
543541

544-
setLaunchDetails('Preparing to launch..')
542+
setLaunchDetails(Lang.queryJS('landing.dlAsync.preparingToLaunch'))
545543

546544
const mojangIndexProcessor = new MojangIndexProcessor(
547545
ConfigManager.getCommonDirectory(),
@@ -559,7 +557,7 @@ async function dlAsync(login = true) {
559557
const authUser = ConfigManager.getSelectedAccount()
560558
loggerLaunchSuite.info(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`)
561559
let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion())
562-
setLaunchDetails('Launching game..')
560+
setLaunchDetails(Lang.queryJS('landing.dlAsync.launchingGame'))
563561

564562
// const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/
565563
const SERVER_JOINED_REGEX = new RegExp(`\\[.+\\]: \\[CHAT\\] ${authUser.displayName} joined the game`)
@@ -604,7 +602,7 @@ async function dlAsync(login = true) {
604602
data = data.trim()
605603
if(data.indexOf('Could not find or load main class net.minecraft.launchwrapper.Launch') > -1){
606604
loggerLaunchSuite.error('Game launch failed, LaunchWrapper was not downloaded properly.')
607-
showLaunchFailure('Error During Launch', 'The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.<br><br>To fix this issue, temporarily turn off your antivirus software and launch the game again.<br><br>If you have time, please <a href="https://github.com/dscalzi/HeliosLauncher/issues">submit an issue</a> and let us know what antivirus software you use. We\'ll contact them and try to straighten things out.')
605+
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.launchWrapperNotDownloaded'))
608606
}
609607
}
610608

@@ -616,7 +614,7 @@ async function dlAsync(login = true) {
616614
proc.stdout.on('data', tempListener)
617615
proc.stderr.on('data', gameErrorListener)
618616

619-
setLaunchDetails('Done. Enjoy the server!')
617+
setLaunchDetails(Lang.queryJS('landing.dlAsync.doneEnjoyServer'))
620618

621619
// Init Discord Hook
622620
if(distro.rawDistribution.discord != null && serv.rawServerdiscord != null){
@@ -633,7 +631,7 @@ async function dlAsync(login = true) {
633631
} catch(err) {
634632

635633
loggerLaunchSuite.error('Error during launch', err)
636-
showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.')
634+
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.checkConsoleForDetails'))
637635

638636
}
639637
}
@@ -740,7 +738,7 @@ let newsLoadingListener = null
740738
*/
741739
function setNewsLoading(val){
742740
if(val){
743-
const nLStr = 'Checking for News'
741+
const nLStr = Lang.queryJS('landing.news.checking')
744742
let dotStr = '..'
745743
nELoadSpan.innerHTML = nLStr + dotStr
746744
newsLoadingListener = setInterval(() => {
@@ -955,7 +953,7 @@ function displayArticle(articleObject, index){
955953
text.style.display = text.style.display === 'block' ? 'none' : 'block'
956954
}
957955
})
958-
newsNavigationStatus.innerHTML = index + ' of ' + newsArr.length
956+
newsNavigationStatus.innerHTML = Lang.query('ejs.landing.newsNavigationStatus', {currentPage: index, totalPages: newsArr.length})
959957
newsContent.setAttribute('article', index-1)
960958
}
961959

0 commit comments

Comments
 (0)