Skip to content

Commit d556004

Browse files
committed
Handle translation loading in translation class
Let's try to keep as much as possible of the translation handling in a single place for clarity.
1 parent 681632b commit d556004

File tree

3 files changed

+115
-48
lines changed

3 files changed

+115
-48
lines changed

app/localization.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@ export class Localizer {
2020
}
2121

2222
// Configure suitable language based on user preferences
23-
setup(supportedLanguages) {
23+
async setup(supportedLanguages, baseURL) {
2424
this.language = 'en'; // Default: US English
25+
this.dictionary = undefined;
26+
27+
this._setupLanguage(supportedLanguages);
28+
await this._setupDictionary(baseURL);
29+
}
2530

31+
_setupLanguage(supportedLanguages) {
2632
/*
2733
* Navigator.languages only available in Chrome (32+) and FireFox (32+)
2834
* Fall back to navigator.language for other browsers
@@ -83,6 +89,27 @@ export class Localizer {
8389
}
8490
}
8591

92+
async _setupDictionary(baseURL) {
93+
if (baseURL) {
94+
if (!baseURL.endsWith("/")) {
95+
baseURL = baseURL + "/";
96+
}
97+
} else {
98+
baseURL = "";
99+
}
100+
101+
if (this.language === "en") {
102+
return;
103+
}
104+
105+
let response = await fetch(baseURL + this.language + ".json");
106+
if (!response.ok) {
107+
throw Error("" + response.status + " " + response.statusText);
108+
}
109+
110+
this.dictionary = await response.json();
111+
}
112+
86113
// Retrieve localised text
87114
get(id) {
88115
if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) {

app/ui.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,20 +1763,8 @@ const UI = {
17631763

17641764
// Set up translations
17651765
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
1766-
l10n.setup(LINGUAS);
1767-
if (l10n.language === "en" || l10n.dictionary !== undefined) {
1768-
UI.prime();
1769-
} else {
1770-
fetch('app/locale/' + l10n.language + '.json')
1771-
.then((response) => {
1772-
if (!response.ok) {
1773-
throw Error("" + response.status + " " + response.statusText);
1774-
}
1775-
return response.json();
1776-
})
1777-
.then((translations) => { l10n.dictionary = translations; })
1778-
.catch(err => Log.Error("Failed to load translations: " + err))
1779-
.then(UI.prime);
1780-
}
1766+
l10n.setup(LINGUAS)
1767+
.catch(err => Log.Error("Failed to load translations: " + err))
1768+
.then(UI.prime);
17811769

17821770
export default UI;

tests/test.localization.js

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@ import _, { Localizer, l10n } from '../app/localization.js';
44
describe('Localization', function () {
55
"use strict";
66

7+
let origNavigator;
8+
let fetch;
9+
10+
beforeEach(function () {
11+
// window.navigator is a protected read-only property in many
12+
// environments, so we need to redefine it whilst running these
13+
// tests.
14+
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
15+
16+
Object.defineProperty(window, "navigator", {value: {}});
17+
window.navigator.languages = [];
18+
19+
fetch = sinon.stub(window, "fetch");
20+
fetch.resolves(new Response("{}"));
21+
});
22+
afterEach(function () {
23+
fetch.restore();
24+
25+
Object.defineProperty(window, "navigator", origNavigator);
26+
});
27+
728
describe('Singleton', function () {
829
it('should export a singleton object', function () {
930
expect(l10n).to.be.instanceOf(Localizer);
@@ -16,77 +37,108 @@ describe('Localization', function () {
1637
});
1738

1839
describe('language selection', function () {
19-
let origNavigator;
20-
beforeEach(function () {
21-
// window.navigator is a protected read-only property in many
22-
// environments, so we need to redefine it whilst running these
23-
// tests.
24-
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
25-
26-
Object.defineProperty(window, "navigator", {value: {}});
27-
window.navigator.languages = [];
28-
});
29-
afterEach(function () {
30-
Object.defineProperty(window, "navigator", origNavigator);
31-
});
32-
3340
it('should use English by default', function () {
3441
let lclz = new Localizer();
3542
expect(lclz.language).to.equal('en');
3643
});
37-
it('should use English if no user language matches', function () {
44+
it('should use English if no user language matches', async function () {
3845
window.navigator.languages = ["nl", "de"];
3946
let lclz = new Localizer();
40-
lclz.setup(["es", "fr"]);
47+
await lclz.setup(["es", "fr"]);
4148
expect(lclz.language).to.equal('en');
4249
});
43-
it('should fall back to generic English for other English', function () {
50+
it('should fall back to generic English for other English', async function () {
4451
window.navigator.languages = ["en-AU", "de"];
4552
let lclz = new Localizer();
46-
lclz.setup(["de", "fr", "en-GB"]);
53+
await lclz.setup(["de", "fr", "en-GB"]);
4754
expect(lclz.language).to.equal('en');
4855
});
49-
it('should prefer specific English over generic', function () {
56+
it('should prefer specific English over generic', async function () {
5057
window.navigator.languages = ["en-GB", "de"];
5158
let lclz = new Localizer();
52-
lclz.setup(["de", "en-AU", "en-GB"]);
59+
await lclz.setup(["de", "en-AU", "en-GB"]);
5360
expect(lclz.language).to.equal('en-GB');
5461
});
55-
it('should use the most preferred user language', function () {
62+
it('should use the most preferred user language', async function () {
5663
window.navigator.languages = ["nl", "de", "fr"];
5764
let lclz = new Localizer();
58-
lclz.setup(["es", "fr", "de"]);
65+
await lclz.setup(["es", "fr", "de"]);
5966
expect(lclz.language).to.equal('de');
6067
});
61-
it('should prefer sub-languages languages', function () {
68+
it('should prefer sub-languages languages', async function () {
6269
window.navigator.languages = ["pt-BR"];
6370
let lclz = new Localizer();
64-
lclz.setup(["pt", "pt-BR"]);
71+
await lclz.setup(["pt", "pt-BR"]);
6572
expect(lclz.language).to.equal('pt-BR');
6673
});
67-
it('should fall back to language "parents"', function () {
74+
it('should fall back to language "parents"', async function () {
6875
window.navigator.languages = ["pt-BR"];
6976
let lclz = new Localizer();
70-
lclz.setup(["fr", "pt", "de"]);
77+
await lclz.setup(["fr", "pt", "de"]);
7178
expect(lclz.language).to.equal('pt');
7279
});
73-
it('should not use specific language when user asks for a generic language', function () {
80+
it('should not use specific language when user asks for a generic language', async function () {
7481
window.navigator.languages = ["pt", "de"];
7582
let lclz = new Localizer();
76-
lclz.setup(["fr", "pt-BR", "de"]);
83+
await lclz.setup(["fr", "pt-BR", "de"]);
7784
expect(lclz.language).to.equal('de');
7885
});
79-
it('should handle underscore as a separator', function () {
86+
it('should handle underscore as a separator', async function () {
8087
window.navigator.languages = ["pt-BR"];
8188
let lclz = new Localizer();
82-
lclz.setup(["pt_BR"]);
89+
await lclz.setup(["pt_BR"]);
8390
expect(lclz.language).to.equal('pt_BR');
8491
});
85-
it('should handle difference in case', function () {
92+
it('should handle difference in case', async function () {
8693
window.navigator.languages = ["pt-br"];
8794
let lclz = new Localizer();
88-
lclz.setup(["pt-BR"]);
95+
await lclz.setup(["pt-BR"]);
8996
expect(lclz.language).to.equal('pt-BR');
9097
});
9198
});
99+
100+
describe('Translation loading', function () {
101+
it('should not fetch a translation for English', async function () {
102+
window.navigator.languages = [];
103+
let lclz = new Localizer();
104+
await lclz.setup([]);
105+
expect(fetch).to.not.have.been.called;
106+
});
107+
it('should fetch dictionary relative base URL', async function () {
108+
window.navigator.languages = ["de", "fr"];
109+
fetch.resolves(new Response('{ "Foobar": "gazonk" }'));
110+
let lclz = new Localizer();
111+
await lclz.setup(["ru", "fr"], "/some/path/");
112+
expect(fetch).to.have.been.calledOnceWith("/some/path/fr.json");
113+
expect(lclz.get("Foobar")).to.equal("gazonk");
114+
});
115+
it('should handle base URL without trailing slash', async function () {
116+
window.navigator.languages = ["de", "fr"];
117+
fetch.resolves(new Response('{ "Foobar": "gazonk" }'));
118+
let lclz = new Localizer();
119+
await lclz.setup(["ru", "fr"], "/some/path");
120+
expect(fetch).to.have.been.calledOnceWith("/some/path/fr.json");
121+
expect(lclz.get("Foobar")).to.equal("gazonk");
122+
});
123+
it('should handle current base URL', async function () {
124+
window.navigator.languages = ["de", "fr"];
125+
fetch.resolves(new Response('{ "Foobar": "gazonk" }'));
126+
let lclz = new Localizer();
127+
await lclz.setup(["ru", "fr"]);
128+
expect(fetch).to.have.been.calledOnceWith("fr.json");
129+
expect(lclz.get("Foobar")).to.equal("gazonk");
130+
});
131+
it('should fail if dictionary cannot be found', async function () {
132+
window.navigator.languages = ["de", "fr"];
133+
fetch.resolves(new Response('{}', { status: 404 }));
134+
let lclz = new Localizer();
135+
let ok = false;
136+
try {
137+
await lclz.setup(["ru", "fr"], "/some/path/");
138+
} catch (e) {
139+
ok = true;
140+
}
141+
expect(ok).to.be.true;
142+
});
143+
});
92144
});

0 commit comments

Comments
 (0)