Skip to content

Commit

Permalink
Handle translation loading in translation class
Browse files Browse the repository at this point in the history
Let's try to keep as much as possible of the translation handling in a
single place for clarity.
  • Loading branch information
CendioOssman committed May 10, 2023
1 parent 681632b commit 0374b4c
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 53 deletions.
36 changes: 32 additions & 4 deletions app/localization.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ export class Localizer {
this.language = 'en';

// Current dictionary of translations
this.dictionary = undefined;
this._dictionary = undefined;
}

// Configure suitable language based on user preferences
setup(supportedLanguages) {
async setup(supportedLanguages, baseURL) {
this.language = 'en'; // Default: US English
this._dictionary = undefined;

this._setupLanguage(supportedLanguages);
await this._setupDictionary(baseURL);
}

_setupLanguage(supportedLanguages) {
/*
* Navigator.languages only available in Chrome (32+) and FireFox (32+)
* Fall back to navigator.language for other browsers
Expand Down Expand Up @@ -83,10 +89,32 @@ export class Localizer {
}
}

async _setupDictionary(baseURL) {
if (baseURL) {
if (!baseURL.endsWith("/")) {
baseURL = baseURL + "/";
}
} else {
baseURL = "";
}

if (this.language === "en") {
return;
}

let response = await fetch(baseURL + this.language + ".json");
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}

this._dictionary = await response.json();
}

// Retrieve localised text
get(id) {
if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) {
return this.dictionary[id];
if (typeof this._dictionary !== 'undefined' &&
this._dictionary[id]) {
return this._dictionary[id];
} else {
return id;
}
Expand Down
18 changes: 3 additions & 15 deletions app/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -1763,20 +1763,8 @@ const UI = {

// Set up translations
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
l10n.setup(LINGUAS);
if (l10n.language === "en" || l10n.dictionary !== undefined) {
UI.prime();
} else {
fetch('app/locale/' + l10n.language + '.json')
.then((response) => {
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
return response.json();
})
.then((translations) => { l10n.dictionary = translations; })
.catch(err => Log.Error("Failed to load translations: " + err))
.then(UI.prime);
}
l10n.setup(LINGUAS, "app/locale/")
.catch(err => Log.Error("Failed to load translations: " + err))
.then(UI.prime);

export default UI;
122 changes: 88 additions & 34 deletions tests/test.localization.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,89 +4,143 @@ import _, { Localizer, l10n } from '../app/localization.js';
describe('Localization', function () {
"use strict";

let origNavigator;
let fetch;

beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");

Object.defineProperty(window, "navigator", {value: {}});
window.navigator.languages = [];

fetch = sinon.stub(window, "fetch");
fetch.resolves(new Response("{}"));
});
afterEach(function () {
fetch.restore();

Object.defineProperty(window, "navigator", origNavigator);
});

describe('Singleton', function () {
it('should export a singleton object', function () {
expect(l10n).to.be.instanceOf(Localizer);
});
it('should export a singleton translation function', function () {
it('should export a singleton translation function', async function () {
// FIXME: Can we use some spy instead?
l10n.dictionary = { "Foobar": "gazonk" };
window.navigator.languages = ["de"];
fetch.resolves(new Response(JSON.stringify({ "Foobar": "gazonk" })));
await l10n.setup(["de"]);
expect(_("Foobar")).to.equal("gazonk");
});
});

describe('language selection', function () {
let origNavigator;
beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");

Object.defineProperty(window, "navigator", {value: {}});
window.navigator.languages = [];
});
afterEach(function () {
Object.defineProperty(window, "navigator", origNavigator);
});

it('should use English by default', function () {
let lclz = new Localizer();
expect(lclz.language).to.equal('en');
});
it('should use English if no user language matches', function () {
it('should use English if no user language matches', async function () {
window.navigator.languages = ["nl", "de"];
let lclz = new Localizer();
lclz.setup(["es", "fr"]);
await lclz.setup(["es", "fr"]);
expect(lclz.language).to.equal('en');
});
it('should fall back to generic English for other English', function () {
it('should fall back to generic English for other English', async function () {
window.navigator.languages = ["en-AU", "de"];
let lclz = new Localizer();
lclz.setup(["de", "fr", "en-GB"]);
await lclz.setup(["de", "fr", "en-GB"]);
expect(lclz.language).to.equal('en');
});
it('should prefer specific English over generic', function () {
it('should prefer specific English over generic', async function () {
window.navigator.languages = ["en-GB", "de"];
let lclz = new Localizer();
lclz.setup(["de", "en-AU", "en-GB"]);
await lclz.setup(["de", "en-AU", "en-GB"]);
expect(lclz.language).to.equal('en-GB');
});
it('should use the most preferred user language', function () {
it('should use the most preferred user language', async function () {
window.navigator.languages = ["nl", "de", "fr"];
let lclz = new Localizer();
lclz.setup(["es", "fr", "de"]);
await lclz.setup(["es", "fr", "de"]);
expect(lclz.language).to.equal('de');
});
it('should prefer sub-languages languages', function () {
it('should prefer sub-languages languages', async function () {
window.navigator.languages = ["pt-BR"];
let lclz = new Localizer();
lclz.setup(["pt", "pt-BR"]);
await lclz.setup(["pt", "pt-BR"]);
expect(lclz.language).to.equal('pt-BR');
});
it('should fall back to language "parents"', function () {
it('should fall back to language "parents"', async function () {
window.navigator.languages = ["pt-BR"];
let lclz = new Localizer();
lclz.setup(["fr", "pt", "de"]);
await lclz.setup(["fr", "pt", "de"]);
expect(lclz.language).to.equal('pt');
});
it('should not use specific language when user asks for a generic language', function () {
it('should not use specific language when user asks for a generic language', async function () {
window.navigator.languages = ["pt", "de"];
let lclz = new Localizer();
lclz.setup(["fr", "pt-BR", "de"]);
await lclz.setup(["fr", "pt-BR", "de"]);
expect(lclz.language).to.equal('de');
});
it('should handle underscore as a separator', function () {
it('should handle underscore as a separator', async function () {
window.navigator.languages = ["pt-BR"];
let lclz = new Localizer();
lclz.setup(["pt_BR"]);
await lclz.setup(["pt_BR"]);
expect(lclz.language).to.equal('pt_BR');
});
it('should handle difference in case', function () {
it('should handle difference in case', async function () {
window.navigator.languages = ["pt-br"];
let lclz = new Localizer();
lclz.setup(["pt-BR"]);
await lclz.setup(["pt-BR"]);
expect(lclz.language).to.equal('pt-BR');
});
});

describe('Translation loading', function () {
it('should not fetch a translation for English', async function () {
window.navigator.languages = [];
let lclz = new Localizer();
await lclz.setup([]);
expect(fetch).to.not.have.been.called;
});
it('should fetch dictionary relative base URL', async function () {
window.navigator.languages = ["de", "fr"];
fetch.resolves(new Response('{ "Foobar": "gazonk" }'));
let lclz = new Localizer();
await lclz.setup(["ru", "fr"], "/some/path/");
expect(fetch).to.have.been.calledOnceWith("/some/path/fr.json");
expect(lclz.get("Foobar")).to.equal("gazonk");
});
it('should handle base URL without trailing slash', async function () {
window.navigator.languages = ["de", "fr"];
fetch.resolves(new Response('{ "Foobar": "gazonk" }'));
let lclz = new Localizer();
await lclz.setup(["ru", "fr"], "/some/path");
expect(fetch).to.have.been.calledOnceWith("/some/path/fr.json");
expect(lclz.get("Foobar")).to.equal("gazonk");
});
it('should handle current base URL', async function () {
window.navigator.languages = ["de", "fr"];
fetch.resolves(new Response('{ "Foobar": "gazonk" }'));
let lclz = new Localizer();
await lclz.setup(["ru", "fr"]);
expect(fetch).to.have.been.calledOnceWith("fr.json");
expect(lclz.get("Foobar")).to.equal("gazonk");
});
it('should fail if dictionary cannot be found', async function () {
window.navigator.languages = ["de", "fr"];
fetch.resolves(new Response('{}', { status: 404 }));
let lclz = new Localizer();
let ok = false;
try {
await lclz.setup(["ru", "fr"], "/some/path/");
} catch (e) {
ok = true;
}
expect(ok).to.be.true;
});
});
});

0 comments on commit 0374b4c

Please sign in to comment.