mirror of
https://github.com/wassname/talk.git
synced 2026-07-06 05:17:19 +08:00
simplified language negotiation
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import ta from 'timeago.js';
|
||||
import { negotiateLanguages } from 'fluent-langneg/compat';
|
||||
|
||||
import has from 'lodash/has';
|
||||
import get from 'lodash/get';
|
||||
import merge from 'lodash/merge';
|
||||
import first from 'lodash/first';
|
||||
import isUndefined from 'lodash/isUndefined';
|
||||
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/ar';
|
||||
@@ -12,8 +15,8 @@ import 'moment/locale/fr';
|
||||
import 'moment/locale/nl';
|
||||
import 'moment/locale/pt-br';
|
||||
|
||||
import { createStorage } from 'coral-framework/services/storage';
|
||||
|
||||
// timeago
|
||||
import ta from 'timeago.js';
|
||||
import arTA from 'timeago.js/locales/ar';
|
||||
import daTA from 'timeago.js/locales/da';
|
||||
import deTA from 'timeago.js/locales/de';
|
||||
@@ -24,6 +27,7 @@ import pt_BRTA from 'timeago.js/locales/pt_BR';
|
||||
import zh_CNTA from 'timeago.js/locales/zh_CN';
|
||||
import zh_TWTA from 'timeago.js/locales/zh_TW';
|
||||
|
||||
// locales
|
||||
import ar from '../../../locales/ar.yml';
|
||||
import en from '../../../locales/en.yml';
|
||||
import da from '../../../locales/da.yml';
|
||||
@@ -35,8 +39,9 @@ import pt_BR from '../../../locales/pt_BR.yml';
|
||||
import zh_CN from '../../../locales/zh_CN.yml';
|
||||
import zh_TW from '../../../locales/zh_TW.yml';
|
||||
|
||||
const defaultLanguage = process.env.TALK_DEFAULT_LANG;
|
||||
const translations = {
|
||||
export const defaultLocale = process.env.TALK_DEFAULT_LANG.replace(/-/g, '_');
|
||||
|
||||
export const translations = {
|
||||
...ar,
|
||||
...en,
|
||||
...da,
|
||||
@@ -49,84 +54,62 @@ const translations = {
|
||||
...zh_TW,
|
||||
};
|
||||
|
||||
let lang;
|
||||
let timeagoInstance;
|
||||
export const supportedLocales = Object.keys(translations);
|
||||
|
||||
function setLocale(storage, locale) {
|
||||
storage.setItem('locale', locale);
|
||||
}
|
||||
let LOCALE;
|
||||
let TIMEAGO_INSTANCE;
|
||||
|
||||
// detectLanguage will try to get the locale from storage if available,
|
||||
// otherwise will try to get it from the navigator, otherwise, it will fallback
|
||||
// to the default language.
|
||||
function detectLanguage(storage) {
|
||||
try {
|
||||
const lang = storage.getItem('locale') || navigator.language;
|
||||
if (lang) {
|
||||
return lang;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Error while trying to detect language, will fallback to',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
console.warn('Could not detect language, will fallback to', defaultLanguage);
|
||||
return defaultLanguage;
|
||||
}
|
||||
|
||||
// getLocale will get the users locale from the local detector and parse it to a
|
||||
// format we can work with.
|
||||
function getLocale(storage) {
|
||||
// Get the language from the local detector.
|
||||
const lang = detectLanguage(storage);
|
||||
|
||||
// Some language strings come with additional subtags as defined in:
|
||||
//
|
||||
// https://www.ietf.org/rfc/bcp/bcp47.txt
|
||||
//
|
||||
// So we should strip that off if we find it.
|
||||
return lang.split('-')[0];
|
||||
}
|
||||
const detectLanguage = () =>
|
||||
first(
|
||||
negotiateLanguages(navigator.languages, supportedLocales, {
|
||||
defaultLocale,
|
||||
strategy: 'lookup',
|
||||
})
|
||||
);
|
||||
|
||||
export function setupTranslations() {
|
||||
// Setup the translation framework with the storage.
|
||||
const storage = createStorage('localStorage');
|
||||
// locale
|
||||
LOCALE = detectLanguage();
|
||||
|
||||
const locale = getLocale(storage);
|
||||
setLocale(storage, locale);
|
||||
|
||||
// Setting moment
|
||||
moment.locale(locale);
|
||||
|
||||
// Extract language key.
|
||||
lang = locale.split('-')[0];
|
||||
|
||||
// Check if we have a translation in this language.
|
||||
if (!(lang in translations)) {
|
||||
lang = defaultLanguage;
|
||||
}
|
||||
// moment
|
||||
moment.locale(LOCALE);
|
||||
|
||||
// timeago
|
||||
ta.register('ar', arTA);
|
||||
ta.register('es', esTA);
|
||||
ta.register('da', daTA);
|
||||
ta.register('de', deTA);
|
||||
ta.register('fr', frTA);
|
||||
ta.register('nl_NL', nlTA);
|
||||
ta.register('pt_BR', pt_BRTA);
|
||||
ta.register('zh_CN', zh_CNTA);
|
||||
ta.register('zh_TW', zh_TWTA);
|
||||
|
||||
timeagoInstance = ta();
|
||||
ta.register('nl-NL', nlTA);
|
||||
ta.register('pt-BR', pt_BRTA);
|
||||
ta.register('zh-CN', zh_CNTA);
|
||||
ta.register('zh-TW', zh_TWTA);
|
||||
TIMEAGO_INSTANCE = ta();
|
||||
}
|
||||
|
||||
/**
|
||||
* loadTranslations will load the new language pack into the existing ones.
|
||||
*
|
||||
* @param {Object} newTranslations translation object to merge into the existing
|
||||
* languages.
|
||||
*/
|
||||
export function loadTranslations(newTranslations) {
|
||||
// Merge the new translations into the existing translations.
|
||||
merge(translations, newTranslations);
|
||||
|
||||
// Push new languages into the supportedLocales array.
|
||||
Object.keys(newTranslations).forEach(language => {
|
||||
if (!supportedLocales.includes(language)) {
|
||||
supportedLocales.push(language);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function timeago(time) {
|
||||
return timeagoInstance.format(new Date(time), lang);
|
||||
return TIMEAGO_INSTANCE.format(new Date(time), LOCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,24 +123,24 @@ export function timeago(time) {
|
||||
*/
|
||||
export function t(key, ...replacements) {
|
||||
let translation;
|
||||
if (has(translations[lang], key)) {
|
||||
translation = get(translations[lang], key);
|
||||
if (has(translations[LOCALE], key)) {
|
||||
translation = get(translations[LOCALE], key);
|
||||
} else if (has(translations['en'], key)) {
|
||||
translation = get(translations['en'], key);
|
||||
console.warn(`${lang}.${key} language key not set`);
|
||||
console.warn(`${LOCALE}.${key} language key not set`);
|
||||
}
|
||||
|
||||
if (translation) {
|
||||
// replace any {n} with the arguments passed to this method
|
||||
replacements.forEach((str, i) => {
|
||||
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
|
||||
});
|
||||
|
||||
return translation;
|
||||
} else {
|
||||
console.warn(`${lang}.${key} and en.${key} language key not set`);
|
||||
if (!translation) {
|
||||
console.warn(`${LOCALE}.${key} and en.${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
|
||||
// Handle replacements in the translation string.
|
||||
return translation.replace(
|
||||
/{(\d+)}/g,
|
||||
(match, number) =>
|
||||
!isUndefined(replacements[number]) ? replacements[number] : match
|
||||
);
|
||||
}
|
||||
|
||||
export default t;
|
||||
|
||||
+2
-2
@@ -413,7 +413,7 @@ fi_FI:
|
||||
title_reject: "Huomasimme sinun hylänneen käyttäjänimen"
|
||||
suspend_user: "Aseta väliaikainen käyttökielto"
|
||||
yes_suspend: "Kyllä, sulje väliaikaisesti"
|
||||
email_message_reject: "Toinen yhteisön jäsen on ilmiantanut käyttäjänimesi ja sen perusteella nimi on hylätty. Et voi enää osallistua keskusteluun. Ole ystävällisesti yhteydessä meihin, jos sinulla on asiasta kysyttävää."
|
||||
email_message_reject: "Toinen yhteisön jäsen on ilmiantanut käyttäjänimesi ja sen perusteella nimi on hylätty. Et voi enää osallistua keskusteluun. Ole ystävällisesti yhteydessä meihin, jos sinulla on asiasta kysyttävää."
|
||||
write_message: "Kirjoita viesti"
|
||||
send: Lähetä
|
||||
thank_you: "Arvostamme palautettasi. Moderaattorimme käy läpi tekemäsi ilmiannon."
|
||||
@@ -462,4 +462,4 @@ fi_FI:
|
||||
close: "Sulje asennusnäkymä"
|
||||
admin_sidebar:
|
||||
view_options: "Näytä asetukset"
|
||||
sort_comments: "Järjestä kommentit"
|
||||
sort_comments: "Järjestä kommentit"
|
||||
|
||||
+1
-1
@@ -58,7 +58,6 @@
|
||||
"dependencies": {
|
||||
"@coralproject/gql-merge": "^0.1.0",
|
||||
"@coralproject/graphql-anywhere-optimized": "^0.1.0",
|
||||
"accepts": "^1.3.4",
|
||||
"apollo-client": "^1.9.1",
|
||||
"apollo-engine": "^0.8.1",
|
||||
"apollo-server-express": "^1.2.0",
|
||||
@@ -108,6 +107,7 @@
|
||||
"express-static-gzip": "^0.3.1",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^0.11.2",
|
||||
"fluent-langneg": "^0.1.0",
|
||||
"form-data": "^2.3.1",
|
||||
"fs-extra": "^4.0.1",
|
||||
"graphql": "^0.10.1",
|
||||
|
||||
+45
-31
@@ -1,8 +1,11 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const debug = require('debug')('talk:services:i18n');
|
||||
const accepts = require('accepts');
|
||||
const { get, has, merge } = require('lodash');
|
||||
const {
|
||||
acceptedLanguages,
|
||||
negotiateLanguages,
|
||||
} = require('fluent-langneg/compat');
|
||||
const { first, get, has, merge, isUndefined } = require('lodash');
|
||||
const yaml = require('yamljs');
|
||||
const plugins = require('./plugins');
|
||||
const { DEFAULT_LANG } = require('../config');
|
||||
@@ -11,7 +14,7 @@ const resolve = (...paths) =>
|
||||
path.resolve(path.join(__dirname, '..', 'locales', ...paths));
|
||||
|
||||
// Load all the translations.
|
||||
let translations = fs
|
||||
const translations = fs
|
||||
.readdirSync(resolve())
|
||||
|
||||
// Resolve all the filenames relative the the locales directory.
|
||||
@@ -23,26 +26,22 @@ let translations = fs
|
||||
// Load the translation files from disk.
|
||||
.map(filename => fs.readFileSync(filename, 'utf8'))
|
||||
|
||||
// Load the translation files.
|
||||
.reduce((packs, contents) => {
|
||||
const pack = yaml.parse(contents);
|
||||
|
||||
return merge(packs, pack);
|
||||
}, {});
|
||||
// Load the translation files and merge the yaml into the existing packs.
|
||||
.reduce((packs, contents) => merge(packs, yaml.parse(contents)), {});
|
||||
|
||||
// Create a list of all supported translations.
|
||||
const languages = Object.keys(translations);
|
||||
const supportedLocales = Object.keys(translations);
|
||||
|
||||
// Move the default language to the front.
|
||||
if (languages.includes(DEFAULT_LANG)) {
|
||||
const from = languages.indexOf(DEFAULT_LANG);
|
||||
languages.splice(from, 1);
|
||||
languages.splice(0, 0, DEFAULT_LANG);
|
||||
if (supportedLocales.includes(DEFAULT_LANG)) {
|
||||
const from = supportedLocales.indexOf(DEFAULT_LANG);
|
||||
supportedLocales.splice(from, 1);
|
||||
supportedLocales.splice(0, 0, DEFAULT_LANG);
|
||||
}
|
||||
debug(`loaded language sets for ${languages}`);
|
||||
debug(`loaded language sets for ${supportedLocales}`);
|
||||
|
||||
let loadedPluginTranslations = false;
|
||||
const loadPluginTranslations = () => {
|
||||
const lazyLoadPluginTranslations = () => {
|
||||
if (loadedPluginTranslations) {
|
||||
return;
|
||||
}
|
||||
@@ -55,7 +54,15 @@ const loadPluginTranslations = () => {
|
||||
|
||||
const pack = yaml.parse(fs.readFileSync(filename, 'utf8'));
|
||||
|
||||
translations = merge(translations, pack);
|
||||
// Merge the translations into the system translations.
|
||||
merge(translations, pack);
|
||||
|
||||
// Push new languages into the supportedLocales array.
|
||||
Object.keys(pack).forEach(language => {
|
||||
if (!supportedLocales.includes(language)) {
|
||||
supportedLocales.push(language);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
loadedPluginTranslations = true;
|
||||
@@ -64,7 +71,7 @@ const loadPluginTranslations = () => {
|
||||
const t = lang => (key, ...replacements) => {
|
||||
// Loads the translations into the translations array from plugins. This is
|
||||
// done lazily to ensure that we don't have an import cycle.
|
||||
loadPluginTranslations();
|
||||
lazyLoadPluginTranslations();
|
||||
|
||||
let translation;
|
||||
if (has(translations[lang], key)) {
|
||||
@@ -74,16 +81,17 @@ const t = lang => (key, ...replacements) => {
|
||||
console.warn(`${lang}.${key} language key not set`);
|
||||
}
|
||||
|
||||
if (translation) {
|
||||
// replace any {n} with the arguments passed to this method
|
||||
replacements.forEach((str, i) => {
|
||||
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
|
||||
});
|
||||
return translation;
|
||||
} else {
|
||||
if (!translation) {
|
||||
console.warn(`${lang}.${key} and en.${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
|
||||
// Handle replacements in the translation string.
|
||||
return translation.replace(
|
||||
/{(\d+)}/g,
|
||||
(match, number) =>
|
||||
!isUndefined(replacements[number]) ? replacements[number] : match
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -92,13 +100,19 @@ const t = lang => (key, ...replacements) => {
|
||||
*/
|
||||
const i18n = {
|
||||
request(req) {
|
||||
debug(`possible languages given request '${accepts(req).languages()}'`);
|
||||
const lang = accepts(req).language(languages);
|
||||
debug(`parsed request language as '${lang}'`);
|
||||
const language = lang ? lang : DEFAULT_LANG;
|
||||
debug(`decided language as '${language}'`);
|
||||
const acceptsLanguages = acceptedLanguages(req.headers['accept-language']);
|
||||
debug(`possible languages given request '${acceptsLanguages}'`);
|
||||
|
||||
return t(language);
|
||||
// negotiate the language.
|
||||
const lang = first(
|
||||
negotiateLanguages(acceptsLanguages, supportedLocales, {
|
||||
defaultLocale: DEFAULT_LANG,
|
||||
strategy: 'lookup',
|
||||
})
|
||||
);
|
||||
debug(`decided language as '${lang}'`);
|
||||
|
||||
return t(lang);
|
||||
},
|
||||
t: t(DEFAULT_LANG),
|
||||
};
|
||||
|
||||
@@ -167,7 +167,7 @@ abbrev@1, abbrev@^1.0.7:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
||||
accepts@^1.3.4, accepts@~1.3.4:
|
||||
accepts@~1.3.4:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
|
||||
dependencies:
|
||||
@@ -3975,6 +3975,10 @@ flexbuffer@0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/flexbuffer/-/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30"
|
||||
|
||||
fluent-langneg@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fluent-langneg/-/fluent-langneg-0.1.0.tgz#aa12054fbfa4b728daec38331efc12f01faae93a"
|
||||
|
||||
flush-write-stream@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
|
||||
|
||||
Reference in New Issue
Block a user