9120e874 Andy McCall 25 days ago
1
2
3
const statusElement = document . getElementById ( ' status' ) ;
4
const output = document . getElementById ( ' output' ) ;
5
const canvas = document . getElementById ( ' canvas' ) ;
6
const code = document . getElementById ( ' code' ) ;
7
const progressElement = document . getElementById ( ' progress' ) ;
8
const spinnerElement = document . getElementById ( ' spinner' ) ;
9
const volumeElementFullScreen = document . getElementById ( ' fullscreen_volume_icon' ) ;
10
const volumeElement = document . getElementById ( ' volume_icon' ) ;
11
12
13
14
var audioContext ;
15
16
window . addEventListener ( ' load' , init , false ) ;
17
function init ( ) {
18
19
try {
20
window . AudioContext = window . AudioContext || window . webkitAudioContext ;
21
audioContext = new AudioContext ( ) ;
22
}
23
catch ( e ) {
24
console . log ( " AudioContext not supported on this Browser." )
25
}
26
}
27
28
29
30
31
const layouts = [
32
' en-us' ,
33
' en-us-int' ,
34
' en-gb' ,
35
' sv' ,
36
' de' ,
37
' da' ,
38
' it' ,
39
' pl' ,
40
' nb' ,
41
' hu' ,
42
' es' ,
43
' fi' ,
44
' pt-br' ,
45
' cz' ,
46
' jp' ,
47
' fr' ,
48
' de-ch' ,
49
' en-us-dvo' ,
50
' et' ,
51
' fr-be' ,
52
' fr-ca' ,
53
' is' ,
54
' pt' ,
55
' hr' ,
56
' sk' ,
57
' sl' ,
58
' lv' ,
59
' lt'
60
] ;
61
62
lang = getFirstBrowserLanguage ( ) . toLowerCase ( ) . trim ( ) ;
63
64
if ( layouts . includes ( lang ) ) {
65
logOutput ( ' Using keyboard map: ' + lang ) ;
66
} else {
67
logOutput ( ' Language (' + lang + ' ) not found in keymaps so using keyboard map: en-us' ) ;
68
lang = ' en-us' ;
69
}
70
71
var url = new URL ( window . location . href ) ;
72
var manifest_link = url . searchParams . get ( " manifest" ) ;
73
var ram_val = url . searchParams . get ( " ram" ) ;
74
var cpu_val = url . searchParams . get ( " cpu" ) ;
75
var mhz_val = url . searchParams . get ( " mhz" ) ;
76
var lang_val = url . searchParams . get ( " keymap" ) ;
77
var model_val = url . searchParams . get ( " model" ) ;
78
var diag = url . searchParams . get ( " longpwron" ) ;
79
var wide = url . searchParams . get ( " widescreen" ) ;
80
var capture = url . searchParams . get ( " capture" ) ;
81
var midline = url . searchParams . get ( " midlineeffects" ) ;
82
var joy1 = url . searchParams . get ( " joy1" ) ;
83
var joy2 = url . searchParams . get ( " joy2" ) ;
84
var joy3 = url . searchParams . get ( " joy3" ) ;
85
var joy4 = url . searchParams . get ( " joy4" ) ;
86
87
var gen1 = true ;
88
89
if ( lang_val ) {
90
lang = lang_val ;
91
}
92
93
var emuArguments = [ ' -keymap' , lang , ' -rtc' ] ;
94
95
if ( model_val ) {
96
if ( model_val === ' gs' ) {
97
gen1 = false ;
98
emuArguments . push ( ' -gs' ) ;
99
}
100
}
101
if ( ram_val ) {
102
emuArguments . push ( ' -ram' , ram_val ) ;
103
}
104
if ( cpu_val && gen1 ) {
105
if ( cpu_val == ' c816' ) {
106
emuArguments . push ( ' -c816' ) ;
107
} else if ( cpu_val == ' c02' ) {
108
emuArguments . push ( ' -c02' ) ;
109
}
110
}
111
if ( mhz_val ) {
112
emuArguments . push ( ' -mhz' , mhz_val ) ;
113
}
114
if ( diag != null ) {
115
emuArguments . push ( ' -longpwron' ) ;
116
}
117
if ( wide != null ) {
118
emuArguments . push ( ' -widescreen' ) ;
119
}
120
if ( capture != null ) {
121
emuArguments . push ( ' -capture' ) ;
122
}
123
if ( midline != null ) {
124
emuArguments . push ( ' -midline-effects' ) ;
125
}
126
if ( joy1 != null ) {
127
emuArguments . push ( ' -joy1' ) ;
128
}
129
if ( joy2 != null ) {
130
emuArguments . push ( ' -joy2' ) ;
131
}
132
if ( joy3 != null ) {
133
emuArguments . push ( ' -joy3' ) ;
134
}
135
if ( joy4 != null ) {
136
emuArguments . push ( ' -joy4' ) ;
137
}
138
logOutput ( " Emulator arguments: " + emuArguments ) ;
139
140
if ( manifest_link ) {
141
openFs ( ) ;
142
}
143
144
var Module = {
145
146
preRun : [
147
function ( ) {
148
ENV . SDL_EMSCRIPTEN_KEYBOARD_ELEMENT = " #canvas" ;
149
} ,
150
function ( ) {
151
152
if ( manifest_link ) {
153
loadManifestLink ( ) ;
154
}
155
}
156
] ,
157
postRun : [
158
function ( ) {
159
canvas . focus ( ) ;
160
}
161
] ,
162
arguments : emuArguments ,
163
print : ( function ( ) {
164
165
if ( output ) output . value = ' ' ;
166
return function ( text ) {
167
if ( arguments . length > 1 ) text = Array . prototype . slice . call ( arguments ) . join ( ' ' ) ;
168
logOutput ( text ) ;
169
} ;
170
} ) ( ) ,
171
printErr : function ( text ) {
172
if ( arguments . length > 1 ) text = Array . prototype . slice . call ( arguments ) . join ( ' ' ) ;
173
174
175
if ( text . startsWith ( ' wasm streaming compile failed:' ) ||
176
text . startsWith ( ' falling back to ArrayBuffer instantiation' ) ||
177
text . startsWith ( ' Calling stub instead of sigaction' ) ) {
178
logOutput ( " [known behavior] " + text ) ;
179
return ;
180
}
181
182
logError ( text ) ;
183
184
185
} ,
186
canvas : ( function ( ) {
187
188
189
190
191
canvas . addEventListener ( " webglcontextlost" , function ( e ) {
192
alert ( ' WebGL context lost. You will need to reload the page.' ) ;
193
e . preventDefault ( ) ;
194
} , false ) ;
195
return canvas ;
196
} ) ( ) ,
197
setStatus : function ( text ) {
198
if ( ! Module . setStatus . last ) Module . setStatus . last = {
199
time : Date . now ( ) ,
200
text : ' '
201
} ;
202
if ( text === Module . setStatus . last . text ) return ;
203
const m = text . match ( / ( [ ^ (] + ) \( ( \d + ( \. \d + ) ? ) \/ ( \d + ) \) / ) ;
204
let now = Date . now ( ) ;
205
if ( m && now - Module . setStatus . last . time < 30 ) return ;
206
Module . setStatus . last . time = now ;
207
Module . setStatus . last . text = text ;
208
if ( m ) {
209
text = m [ 1 ] ;
210
progressElement . value = parseInt ( m [ 2 ] ) * 100 ;
211
progressElement . max = parseInt ( m [ 4 ] ) * 100 ;
212
progressElement . hidden = false ;
213
spinnerElement . hidden = false ;
214
} else {
215
progressElement . value = null ;
216
progressElement . max = null ;
217
progressElement . hidden = true ;
218
if ( ! text ) spinnerElement . hidden = true ;
219
}
220
statusElement . innerHTML = text ;
221
logOutput ( text ) ;
222
} ,
223
totalDependencies : 0 ,
224
monitorRunDependencies : function ( left ) {
225
this . totalDependencies = Math . max ( this . totalDependencies , left ) ;
226
Module . setStatus ( left ? ' Preparing... (' + ( this . totalDependencies - left ) + ' /' + this . totalDependencies + ' )' : ' All downloads complete.' ) ;
227
}
228
} ;
229
230
Module . setStatus ( ' Downloading file...' ) ;
231
logOutput ( ' Downloading file...' ) ;
232
233
window . onerror = function ( ) {
234
235
spinnerElement . style . display = ' none' ;
236
Module . setStatus = function ( text ) {
237
if ( text ) Module . printErr ( ' [post-exception status] ' + text ) ;
238
} ;
239
} ;
240
241
function loadManifestLink ( ) {
242
addRunDependency ( ' load-manifest-link' ) ;
243
console . log ( " Loading URL:" , manifest_link ) ;
244
fetch ( manifest_link )
245
. then ( function ( response ) {
246
var disposition = response . headers . get ( ' Content-Disposition' ) ;
247
if ( disposition ) {
248
var filename = parseDispositionFilename ( disposition ) ;
249
if ( filename ) {
250
console . log ( " Loading filename:" , filename ) ;
251
if ( filename . toLowerCase ( ) . endsWith ( ' .bas' ) ) {
252
console . log ( " Loading from BAS." ) ;
253
loadBas ( manifest_link , filename ) ;
254
} else if ( filename . toLowerCase ( ) . endsWith ( ' .prg' ) ) {
255
console . log ( " Loading from PRG." ) ;
256
loadPrg ( manifest_link , filename ) ;
257
} else if ( filename . toLowerCase ( ) . endsWith ( ' .zip' ) ) {
258
console . log ( " Loading from ZIP." ) ;
259
loadZip ( manifest_link ) ;
260
}
261
}
262
} else {
263
if ( manifest_link . toLowerCase ( ) . endsWith ( ' .bas' ) ) {
264
var filename = manifest_link . replace ( / ^ . * [ \\ \/ ] / , ' ' ) ;
265
console . log ( " Loading from BAS." ) ;
266
loadBas ( manifest_link , filename ) ;
267
} else if ( manifest_link . toLowerCase ( ) . endsWith ( ' .prg' ) ) {
268
var filename = manifest_link . replace ( / ^ . * [ \\ \/ ] / , ' ' ) ;
269
console . log ( " Loading from PRG." ) ;
270
loadPrg ( manifest_link , filename ) ;
271
} else if ( manifest_link . toLowerCase ( ) . endsWith ( ' .zip' ) ) {
272
console . log ( " Loading from ZIP." ) ;
273
loadZip ( manifest_link ) ;
274
} else {
275
console . log ( " Loading from directory." ) ;
276
if ( ! manifest_link . endsWith ( ' /' ) ) {
277
manifest_link = manifest_link + ' /' ;
278
}
279
loadManifest ( ) ;
280
}
281
}
282
} )
283
. then ( function ( ) {
284
removeRunDependency ( ' load-manifest-link' ) ;
285
} ) ;
286
}
287
288
function parseDispositionFilename ( disposition ) {
289
const utf8FilenameRegex = / filename\* =UTF-8''( [ \w %\- \. ] + ) ( ?: ; ? | $ ) / i ;
290
const asciiFilenameRegex = / ^ filename=( [ "'] ? ) ( . *? [ ^ \\ ] ) \1 ( ?: ; ? | $ ) / i ;
291
292
let fileName = null ;
293
if ( utf8FilenameRegex . test ( disposition ) ) {
294
fileName = decodeURIComponent ( utf8FilenameRegex . exec ( disposition ) [ 1 ] ) ;
295
} else {
296
297
298
const filenameStart = disposition . toLowerCase ( ) . indexOf ( ' filename=' ) ;
299
if ( filenameStart >= 0 ) {
300
const partialDisposition = disposition . slice ( filenameStart ) ;
301
const matches = asciiFilenameRegex . exec ( partialDisposition ) ;
302
if ( matches != null && matches [ 2 ] ) {
303
fileName = matches [ 2 ] ;
304
}
305
}
306
}
307
return fileName ;
308
}
309
310
function loadBas ( basFileUrl , filename ) {
311
console . log ( ' Adding start BAS:' , filename )
312
emuArguments . push ( ' -bas' , filename , ' -run' ) ;
313
FS . createPreloadedFile ( ' /' , filename , basFileUrl , true , true ) ;
314
console . log ( " Starting Emulator..." )
315
console . log ( " Emulator arguments: " , emuArguments )
316
}
317
318
function loadPrg ( prgFileUrl , filename ) {
319
console . log ( ' Adding start PRG:' , filename )
320
emuArguments . push ( ' -prg' , filename , ' -run' ) ;
321
FS . createPreloadedFile ( ' /' , filename , prgFileUrl , true , true ) ;
322
console . log ( " Starting Emulator..." )
323
console . log ( " Emulator arguments: " , emuArguments )
324
}
325
326
function loadZip ( zipFileUrl ) {
327
addRunDependency ( ' load-zip' ) ;
328
fetch ( zipFileUrl )
329
. then ( function ( response ) {
330
if ( response . status === 200 || response . status === 0 ) {
331
return Promise . resolve ( response . blob ( ) ) ;
332
} else {
333
return Promise . reject ( new Error ( response . statusText ) ) ;
334
335
}
336
} )
337
. then ( JSZip . loadAsync )
338
. then ( extractManifestFromBuffer )
339
. then ( function ( ) {
340
console . log ( " Starting Emulator..." )
341
console . log ( " Emulator arguments: " , emuArguments )
342
removeRunDependency ( ' load-zip' ) ;
343
} ) ;
344
}
345
346
function extractManifestFromBuffer ( zip ) {
347
if ( zip . file ( " manifest.json" ) == null ) {
348
console . log ( " Unable to find manifest.json file. Writing all files from zip." ) ;
349
const promises = [ ] ;
350
writeAllFilesFromZip ( zip , promises , [ ] ) ;
351
return Promise . all ( promises ) . then ( ( value ) => {
352
console . log ( " Emulator filesystem loading complete." )
353
} ) ;
354
}
355
else {
356
return zip . file ( " manifest.json" ) . async ( " uint8array" )
357
. then ( function ( content ) {
358
let manifestString = new TextDecoder ( " utf-8" ) . decode ( content ) ;
359
let manifestObject = JSON . parse ( manifestString ) ;
360
console . log ( " Parsed manifest from zip:" )
361
console . log ( manifestObject ) ;
362
363
if ( manifestObject . ram ) {
364
console . log ( ' Found RAM amount: ' + manifestObject . ram ) ;
365
emuArguments . push ( ' -ram' , manifestObject . ram ) ;
366
}
367
368
if ( manifestObject . model ) {
369
console . log ( ' Found model: ' + manifestObject . model ) ;
370
if ( manifestObject . model === ' gs' ) {
371
gen1 = false ;
372
emuArguments . push ( ' -gs' ) ;
373
}
374
}
375
376
if ( gen1 && manifestObject . cpu ) {
377
console . log ( ' Found CPU type: ' + manifestObject . cpu ) ;
378
if ( manifestObject . cpu == ' c816' ) {
379
emuArguments . push ( ' -c816' ) ;
380
} else if ( manifestObject . cpu == ' c02' ) {
381
emuArguments . push ( ' -c02' ) ;
382
}
383
}
384
if ( manifestObject . mhz ) {
385
console . log ( ' Found mhz variable: ' + manifestObject . mhz ) ;
386
emuArguments . push ( ' -mhz' , manifestObject . mhz ) ;
387
}
388
if ( manifestObject . keymap ) {
389
console . log ( ' Found keymap variable: ' + manifestObject . keymap ) ;
390
emuArguments . push ( ' -keymap' , manifestObject . keymap ) ;
391
}
392
if ( manifestObject . longpwron ) {
393
console . log ( ' Found longpwron variable' ) ;
394
emuArguments . push ( ' -longpwron' ) ;
395
}
396
if ( manifestObject . widescreen ) {
397
console . log ( ' Found widescreen variable' ) ;
398
emuArguments . push ( ' -widescreen' ) ;
399
}
400
if ( manifestObject . capture ) {
401
console . log ( ' Found capture variable' ) ;
402
emuArguments . push ( ' -capture' ) ;
403
}
404
if ( manifestObject . midlineeffects ) {
405
console . log ( ' Found midlineeffects variable' ) ;
406
emuArguments . push ( ' -midline-effects' ) ;
407
}
408
if ( manifestObject . joy1 ) {
409
console . log ( ' Found joy1 variable' ) ;
410
emuArguments . push ( ' -joy1' ) ;
411
}
412
if ( manifestObject . joy2 ) {
413
console . log ( ' Found joy2 variable' ) ;
414
emuArguments . push ( ' -joy2' ) ;
415
}
416
if ( manifestObject . joy3 ) {
417
console . log ( ' Found joy3 variable' ) ;
418
emuArguments . push ( ' -joy3' ) ;
419
}
420
if ( manifestObject . joy4 ) {
421
console . log ( ' Found joy4 variable' ) ;
422
emuArguments . push ( ' -joy4' ) ;
423
}
424
425
const promises = [ ] ;
426
if ( manifestObject . resources ) {
427
console . log ( ' Found resources section in manifest.' ) ;
428
var startFiles = [ ] ;
429
manifestObject . resources . forEach ( function ( element ) {
430
let fileName = element . replace ( / ^ . * [ \\ \/ ] / , ' ' ) ;
431
if ( fileName . toLowerCase ( ) . endsWith ( " .bas" ) || fileName . toLowerCase ( ) . endsWith ( " .prg" ) ) {
432
startFiles . push ( fileName ) ;
433
}
434
if ( zip . file ( fileName ) == null ) {
435
logError ( " Unable to find resources entry: " + fileName ) ;
436
logError ( " This is likely an error, check resources section in manifest." )
437
} else {
438
promises . push ( zip . file ( fileName ) . async ( " uint8array" ) . then ( function ( content ) {
439
console . log ( ' Writing to emulator filesystem:' , fileName ) ;
440
try {
441
FS . writeFile ( fileName , content ) ;
442
}
443
catch ( e ) {
444
console . log ( ' Error writing to emulator filesystem:' , file . name ) ;
445
}
446
} ) ) ;
447
}
448
addStartFile ( manifestObject , startFiles ) ;
449
} ) ;
450
} else {
451
console . log ( ' Resources section not found in manifest. Writing all files from zip.' ) ;
452
writeAllFilesFromZip ( zip , promises , manifestObject ) ;
453
}
454
return Promise . all ( promises ) ;
455
} )
456
. then ( ( value ) => {
457
console . log ( " Emulator filesystem loading complete." )
458
} ) ;
459
}
460
}
461
462
function writeAllFilesFromZip ( zip , promises , manifestObject ) {
463
var startFiles = [ ] ;
464
const writeResources = ( zip ) => {
465
zip . forEach ( ( path , file ) => {
466
if ( file . dir ) {
467
FS . mkdirTree ( file . name ) ;
468
writeResources ( zip . folder ( path ) ) ;
469
return ;
470
} else {
471
if ( file . name . toLowerCase ( ) . endsWith ( " .bas" ) || file . name . toLowerCase ( ) . endsWith ( " .prg" ) ) {
472
startFiles . push ( file . name ) ;
473
}
474
}
475
476
promises . push ( zip . file ( path ) . async ( " uint8array" )
477
. then ( function ( content ) {
478
console . log ( ' Writing to emulator filesystem:' , file . name ) ;
479
try {
480
FS . writeFile ( file . name , content ) ;
481
}
482
catch ( e ) {
483
console . log ( ' Error writing to emulator filesystem:' , file . name ) ;
484
}
485
} )
486
) ;
487
} ) ;
488
} ;
489
writeResources ( zip ) ;
490
addStartFile ( manifestObject , startFiles ) ;
491
}
492
493
function loadManifest ( ) {
494
addRunDependency ( ' load-manifest' ) ;
495
fetch ( manifest_link + ' manifest.json' ) . then ( function ( response ) {
496
return response . json ( ) ;
497
} ) . then ( function ( manifest ) {
498
console . log ( " Loading from manifest:" )
499
console . log ( manifest ) ;
500
var startFiles = [ ] ;
501
manifest . resources . forEach ( element => {
502
element = manifest_link + element ;
503
let filename = element . replace ( / ^ . * [ \\ \/ ] / , ' ' )
504
if ( filename . toLowerCase ( ) . endsWith ( " .bas" ) || filename . toLowerCase ( ) . endsWith ( " .prg" ) ) {
505
startFiles . push ( filename ) ;
506
}
507
FS . createPreloadedFile ( ' /' , filename , element , true , true ) ;
508
} ) ;
509
addStartFile ( manifest , startFiles ) ;
510
console . log ( " Starting Emulator..." )
511
console . log ( " Emulator arguments: " , emuArguments )
512
removeRunDependency ( ' load-manifest' ) ;
513
} ) . catch ( function ( ) {
514
console . log ( " Unable to read manifest. Check the manifest http parameter" ) ;
515
} ) ;
516
}
517
518
function addStartFile ( manifestObject , startFiles ) {
519
if ( manifestObject && manifestObject . start_bas && manifestObject . start_prg ) {
520
logError ( " start_bas and start_prg used in manifest" ) ;
521
logError ( " This is likely an error, defaulting to start_bas" )
522
}
523
524
if ( manifestObject && manifestObject . start_bas ) {
525
console . log ( ' Adding start BAS:' , manifestObject . start_bas )
526
emuArguments . push ( ' -bas' , manifestObject . start_bas , ' -run' ) ;
527
} else if ( manifestObject && manifestObject . start_prg ) {
528
console . log ( ' Adding start PRG:' , manifestObject . start_prg )
529
emuArguments . push ( ' -prg' , manifestObject . start_prg , ' -run' ) ;
530
} else if ( startFiles ) {
531
if ( startFiles . length === 1 ) {
532
533
var filename = startFiles [ 0 ] ;
534
if ( filename . toLowerCase ( ) . endsWith ( " .bas" ) ) {
535
console . log ( ' Adding start BAS:' , filename )
536
emuArguments . push ( ' -bas' , filename , ' -run' ) ;
537
} else if ( filename . toLowerCase ( ) . endsWith ( " .prg" ) ) {
538
console . log ( ' Adding start PRG:' , filename )
539
emuArguments . push ( ' -prg' , filename , ' -run' ) ;
540
}
541
} else {
542
logOutput ( " Start files: " + startFiles ) ;
543
544
}
545
}
546
}
547
548
function toggleAudio ( ) {
549
if ( audioContext && audioContext . state != " running" ) {
550
audioContext . resume ( ) . then ( ( ) => {
551
volumeElement . innerHTML = " volume_up" ;
552
volumeElementFullScreen . innerHTML = " volume_up" ;
553
console . log ( " Resumed Audio." )
554
Module . ccall ( " j2c_start_audio" , " void" , [ " bool" ] , [ true ] ) ;
555
} ) ;
556
} else if ( audioContext && audioContext . state == " running" ) {
557
audioContext . suspend ( ) . then ( function ( ) {
558
console . log ( " Stopped Audio." )
559
volumeElement . innerHTML = " volume_off" ;
560
volumeElementFullScreen . innerHTML = " volume_off" ;
561
Module . ccall ( " j2c_start_audio" , " void" , [ " bool" ] , [ false ] ) ;
562
} ) ;
563
}
564
canvas . focus ( ) ;
565
}
566
567
function resetEmulator ( ) {
568
j2c_reset = Module . cwrap ( " j2c_reset" , " void" , [ ] ) ;
569
j2c_reset ( ) ;
570
canvas . focus ( ) ;
571
}
572
573
function runCode ( ) {
574
Module . ccall ( " j2c_paste" , " void" , [ " string" ] , [ ' \n NEW\n ' + code . value + ' \n RUN\n ' ] ) ;
575
canvas . focus ( ) ;
576
}
577
578
function closeFs ( ) {
579
canvas . parentElement . classList . remove ( " fullscreen" ) ;
580
canvas . focus ( ) ;
581
}
582
583
function openFs ( ) {
584
canvas . parentElement . classList . add ( " fullscreen" ) ;
585
canvas . focus ( ) ;
586
}
587
588
function logOutput ( text ) {
589
if ( output ) {
590
output . innerHTML += text + " \n " ;
591
output . parentElement . scrollTop = output . parentElement . scrollHeight ;
592
}
593
console . log ( text ) ;
594
}
595
596
function logError ( text ) {
597
if ( output ) {
598
output . innerHTML += " [error] " + text + " \n " ;
599
output . parentElement . scrollTop = output . parentElement . scrollHeight ;
600
}
601
console . error ( text ) ;
602
}
603
604
605
606
function getFirstBrowserLanguage ( ) {
607
const nav = window . navigator ,
608
browserLanguagePropertyKeys = [ ' language' , ' browserLanguage' , ' systemLanguage' , ' userLanguage' ] ;
609
let i ,
610
language ;
611
612
613
if ( Array . isArray ( nav . languages ) ) {
614
for ( i = 0 ; i < nav . languages . length ; i ++ ) {
615
language = nav . languages [ i ] ;
616
if ( language && language . length ) {
617
return language ;
618
}
619
}
620
}
621
622
623
for ( i = 0 ; i < browserLanguagePropertyKeys . length ; i ++ ) {
624
language = nav [ browserLanguagePropertyKeys [ i ] ] ;
625
if ( language && language . length ) {
626
return language ;
627
}
628
}
629
630
return null ;
631
}