Skip to content

Commit

Permalink
fix: language support
Browse files Browse the repository at this point in the history
use language detection plugin which detects language setting in query string,
navigator, html tag, path and subdomain. Detection can be disabled, or overwritten
by the administrator or user.

fix #573
  • Loading branch information
sualko committed Feb 11, 2019
1 parent 38635dd commit d31b957
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 40 deletions.
1 change: 1 addition & 0 deletions custom.d.ts
Expand Up @@ -23,3 +23,4 @@ declare var __VERSION__: string;
declare var __BUILD_DATE__: string;
declare var __BUNDLE_NAME__: string;
declare var __DEPENDENCIES__: string;
declare let __LANGS__: string[];
3 changes: 2 additions & 1 deletion karma.conf.js
Expand Up @@ -34,7 +34,8 @@ module.exports = function(config) {
module: webpackConfig.module,
resolve: webpackConfig.resolve,
plugins: [
webpackConfig.plugins[1]
webpackConfig.plugins[1],
webpackConfig.plugins[webpackConfig.plugins.length - 2],
],
node: {
fs: 'empty'
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -91,6 +91,7 @@
"handlebars-loader": "^1.5.0",
"handlebars-runtime": "^1.0.12",
"i18next": "^15.0.1",
"i18next-browser-languagedetector": "^3.0.0",
"jingle": "^3.0.3",
"jingle-session": "^2.0.3",
"jquery": "^3.3.1",
Expand Down
2 changes: 2 additions & 0 deletions src/Client.ts
Expand Up @@ -8,6 +8,7 @@ import PresenceController from './PresenceController'
import PageVisibility from './PageVisibility'
import ChatWindowList from './ui/ChatWindowList';
import AccountManager from './AccountManager';
import Translation from '@util/Translation';

export default class Client {
private static storage: Storage;
Expand Down Expand Up @@ -35,6 +36,7 @@ export default class Client {
Options.overwriteDefaults(options);
}

Translation.initialize();
PageVisibility.init();

let storage = Client.getStorage();
Expand Down
9 changes: 6 additions & 3 deletions src/OptionsDefault.ts
Expand Up @@ -12,8 +12,11 @@ export let appName = 'web applications';
/** How many messages should be logged? */
export let numberOfMessages = 100;

/** Default language */
export let defaultLang = 'en';
/** Language as IETF language tag (e.g. en, de, es) */
export let lang = '';

/** Auto language detection */
export let autoLang = true;

/** Place for roster */
export let rosterAppend = 'body';
Expand Down Expand Up @@ -102,7 +105,7 @@ export let screenMediaExtension = {
chrome: ''
};

/** @TODO (UNUSED) */
/** Options for native browser notifications */
export let notification = {
enable: true,

Expand Down
37 changes: 37 additions & 0 deletions src/ui/dialogs/settings.ts
Expand Up @@ -7,6 +7,7 @@ import List from '../DialogList'
import ListItem from '../DialogListItem'
import AvatarSet from '../AvatarSet'
import Log from '../../util/Log'
import Translation from '@util/Translation';

const ENOUGH_BITS_OF_ENTROPY = 50;

Expand All @@ -26,6 +27,7 @@ class StartPage extends Page {
//@REVIEW could also return Page or getDOM interface?
protected generateContentElement(): JQuery | JQuery[] {
return [
new ClientSection(this.navigation).getDOM(),
new AccountOverviewSection(this.navigation).getDOM()
];
}
Expand All @@ -35,8 +37,43 @@ class ClientSection extends Section {
protected generateContentElement(): JQuery {
let contentElement = new List();

contentElement.append(new ListItem(
Translation.t('Language'),
Translation.t('After_changing_this_option_you_have_to_reload_the_page'),
undefined,
undefined,
this.getLanguageSelectionElement()
));

return contentElement.getDOM();
}

private getLanguageSelectionElement() {
let currentLang = Client.getOption('lang');
let element = $('<select>');
element.append('<option value=""></option>');
__LANGS__.forEach((lang) => {
let optionElement = $('<option>');
optionElement.text(lang);
optionElement.appendTo(element);

if (lang === currentLang) {
optionElement.attr('selected', 'selected');
}
});

element.on('change', (ev) => {
let value = $(ev.target).val();

Client.setOption('lang', value ? value : undefined);
});

if (element.find('[selected]').length === 0) {
element.find('option:eq(0)').attr('selected', 'selected');
}

return element;
}
}

class AccountOverviewSection extends Section {
Expand Down
79 changes: 44 additions & 35 deletions src/util/Translation.ts
@@ -1,49 +1,58 @@
import i18next from 'i18next'
import Log from '@util/Log';
import Client from '@src/Client';
import * as LanguageDetector from 'i18next-browser-languagedetector'

let en = require('../../locales/en.json');
let de = require('../../locales/de.json');

function detectLanguage() {
let lang;

// if (storage.getItem('lang') !== null) {
// lang = storage.getItem('lang');
// } else
if (navigator.languages && navigator.languages.length > 0) {
lang = navigator.languages[0];
} else if (navigator.language) {
lang = navigator.language;
} else {
lang = Client.getOption('defaultLang');
}
let resources = __LANGS__.reduce((resources, lang) => {
resources[lang] = require(`../../locales/${lang}.json`);

return lang;
}
return resources;
}, {});

i18next.init({
lng: detectLanguage(),
fallbackLng: 'en',
returnNull: false,
resources: {
en,
de
},
interpolation: {
prefix: '__',
suffix: '__'
export default class Translation {
private static initialized = false;

public static initialize() {
if (Client.getOption('autoLang')) {
i18next.use(LanguageDetector);
}

i18next.init({
debug: Client.isDebugMode(),
lng: Client.getOption('lang'),
fallbackLng: 'en',
returnNull: false,
resources,
interpolation: {
prefix: '__',
suffix: '__'
},
saveMissing: true,
detection: {
order: ['querystring', 'navigator', 'htmlTag', 'path', 'subdomain'],
},
});

i18next.on('missingKey', function(language, namespace, key, res) {
Log.info(`[i18n] Translation of "${key}" is missing for language "${language}". Namespace: ${namespace}. Resource: ${res}.`);
});

Translation.initialized = true;
}
});

i18next.on('missingKey', function(language, namespace, key, res) {
Log.info(`[i18n] Translation of "${key}" is missing for language "${language}". Namespace: ${namespace}. Resource: ${res}.`);
});

export default class Translation {
public static t(text: string, param?): string {
if (!Translation.initialized) {
Log.warn('Translator not initialized');

return text;
}

let translatedString = i18next.t(text, param);

return translatedString;
}

public static getCurrentLanguage() {
return i18next.language;
}
}
20 changes: 19 additions & 1 deletion webpack.config.js
@@ -1,5 +1,6 @@
/* jshint node:true */
const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
const packageJson = require('./package.json');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
Expand All @@ -8,9 +9,25 @@ const CleanWebpackPlugin = require('clean-webpack-plugin');
const GitRevisionPlugin = new(require('git-revision-webpack-plugin'))();
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

let supportedLangs = fs.readdirSync('./locales/').filter(filename => {
if (!/\.json$/.test(filename)) {
return false;
}

let file = require(`./locales/${filename}`);

for (let key in file.translation) {
if (typeof file.translation[key] === 'string') {
return true;
}
}

return false;
}).map(filename => filename.replace(/\.json$/, ''));

const DEVELOPMENT_MODE = 'development';
const PRODUCTION_MODE = 'production';
const MOMENTJS_LOCALES = ['de', 'jp', 'nl', 'fr', 'zh', 'tr', 'pt', 'el', 'ro', 'pl', 'es', 'ru', 'it', 'hu'];
const MOMENTJS_LOCALES = supportedLangs.map(lang => lang.replace(/-.+/, ''));
const JS_BUNDLE_NAME = 'jsxc.bundle.js';

const dependencies = Object.keys(packageJson.dependencies).map(function(name) {
Expand All @@ -25,6 +42,7 @@ let definePluginConfig = {
__BUILD_DATE__: JSON.stringify(buildDate),
__BUNDLE_NAME__: JSON.stringify(JS_BUNDLE_NAME),
__DEPENDENCIES__: JSON.stringify(dependencies.join(', ')),
__LANGS__: JSON.stringify(supportedLangs),
};

const fileLoader = {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Expand Up @@ -3911,6 +3911,11 @@ husky@^1.1.0:
run-node "^1.0.0"
slash "^2.0.0"

i18next-browser-languagedetector@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-3.0.0.tgz#bdcce9e3f40162cdefc7db469a972c34f224250b"
integrity sha512-WI+XaZ94+WDp7HhIVykTwZUQsyCqpNDGWTIKG4PAeekZ4UVeMX6sN4gQj+Ad5g5PPwwENiynbW8x9TFFStoYPg==

i18next@^15.0.1:
version "15.0.1"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-15.0.1.tgz#214057bd6cdc774e4c2f87f96f1c3d05d3eeafeb"
Expand Down

0 comments on commit d31b957

Please sign in to comment.