Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

persistent profile language #1338

Merged
merged 10 commits into from
Sep 6, 2016
119 changes: 7 additions & 112 deletions client/modules/i18n/main.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import _ from "lodash";
import i18next from "i18next";
import i18nextBrowserLanguageDetector from "i18next-browser-languagedetector";
import i18nextLocalStorageCache from "i18next-localstorage-cache";
import i18nextSprintfPostProcessor from "i18next-sprintf-postprocessor";
import i18nextJquery from "jquery-i18next";
import { Meteor } from "meteor/meteor";
import { Tracker } from "meteor/tracker";
import { Session } from "meteor/session";
import { SimpleSchema } from "meteor/aldeed:simple-schema";
import { Reaction } from "/client/api";
import { Packages, Shops, Translations } from "/lib/collections";
import * as Schemas from "/lib/collections/schemas";
import { Packages, Translations } from "/lib/collections";

//
// Reaction i18n Translations, RTL and Currency Exchange Support
Expand Down Expand Up @@ -40,7 +33,7 @@ export function getBrowserLanguage() {
* @param {String} name - name
* @return {Object} return schema label object
*/
function getLabelsFor(schema, name) {
export function getLabelsFor(schema, name) {
const labels = {};
// loop through all the rendered form fields and generate i18n keys
for (const fieldName of schema._schemaKeys) {
Expand Down Expand Up @@ -68,7 +61,7 @@ function getLabelsFor(schema, name) {
* @todo implement messaging hierarchy from simple-schema
* @return {Object} returns i18n translated message for schema labels
*/
function getMessagesFor() {
export function getMessagesFor() {
const messages = {};
for (const message in SimpleSchema._globalMessages) {
if ({}.hasOwnProperty.call(SimpleSchema._globalMessages, message)) {
Expand All @@ -89,19 +82,14 @@ function getMessagesFor() {
*/
export const i18nextDep = new Tracker.Dependency();
export const localeDep = new Tracker.Dependency();
const packageNamespaces = [];
export const packageNamespaces = [];

Meteor.startup(() => {
Tracker.autorun(function (c) {
// setting local and active packageNamespaces
// packageNamespaces are used to determine i18n namespace
if (Reaction.Subscriptions.Shops.ready()) {
// TODO: i18nextBrowserLanguageDetector
// const defaultLanguage = lng.detect() || shop.language;

// set default session language
Session.setDefault("language", getBrowserLanguage());

// every package gets a namespace, fetch them
// const packageNamespaces = [];
// every package gets a namespace, fetch them and export
const packages = Packages.find({}, {
fields: {
name: 1
Expand All @@ -128,97 +116,4 @@ Meteor.startup(() => {
});
});

// use tracker autorun to detect language changes
Tracker.autorun(function () {
return Meteor.subscribe("Translations", Session.get("language"), () => {
// fetch reaction translations
const translations = Translations.find({}, {
fields: {
_id: 0
}
}).fetch();

// map reduce translations into i18next formatting
const resources = translations.reduce(function (x, y) {
const ns = Object.keys(y.translation)[0];
// first creating the structure, when add additional namespaces
if (x[y.i18n]) {
x[y.i18n][ns] = y.translation[ns];
} else {
x[y.i18n] = y.translation;
}
return x;
}, {});

const shop = Shops.findOne(Reaction.getShopId());

//
// initialize i18next
//
i18next
.use(i18nextBrowserLanguageDetector)
.use(i18nextLocalStorageCache)
.use(i18nextSprintfPostProcessor)
.use(i18nextJquery)
.init({
debug: false,
ns: packageNamespaces, // translation namespace for every package
defaultNS: "core", // reaction "core" is the default namespace
lng: Session.get("language"), // user session language
fallbackLng: shop ? shop.language : null, // Shop language
resources: resources
// saveMissing: true,
// missingKeyHandler: function (lng, ns, key, fallbackValue) {
// Meteor.call("i18n/addTranslation", lng, ns, key, fallbackValue);
// }
}, (err, t) => {
// someday this should work
// see: https://github.com/aldeed/meteor-simple-schema/issues/494
for (const schema in _.omit(Schemas, "__esModule")) {
if ({}.hasOwnProperty.call(Schemas, schema)) {
const ss = Schemas[schema];
ss.labels(getLabelsFor(ss, schema));
ss.messages(getMessagesFor(ss, schema));
}
}

i18nextDep.changed();

// global first time init event finds and replaces
// data-i18n attributes in html/template source.
$elements = $("[data-i18n]").localize();

// apply language direction to html
if (t("languageDirection") === "rtl") {
return $("html").addClass("rtl");
}
return $("html").removeClass("rtl");
});
});
});

//
// init i18nextJquery
//
i18nextJquery.init(i18next, $, {
tName: "t", // --> appends $.t = i18next.t
i18nName: "i18n", // --> appends $.i18n = i18next
handleName: "localize", // --> appends $(selector).localize(opts);
selectorAttr: "data-i18n", // selector for translating elements
targetAttr: "data-i18n-target", // element attribute to grab target element to translate (if diffrent then itself)
parseDefaultValueFromContent: true // parses default values from content ele.val or ele.text
});

// global onRendered event finds and replaces
// data-i18n attributes in html/template source.
// uses methods from i18nextJquery
Template.onRendered(function () {
this.autorun((function () {
return function () {
i18nextDep.depend();
$elements = $("[data-i18n]").localize();
};
})(this));
});

export default i18next;
140 changes: 140 additions & 0 deletions client/modules/i18n/startup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import _ from "lodash";
import i18nextBrowserLanguageDetector from "i18next-browser-languagedetector";
import i18nextLocalStorageCache from "i18next-localstorage-cache";
import i18nextSprintfPostProcessor from "i18next-sprintf-postprocessor";
import i18nextJquery from "jquery-i18next";
import { Meteor } from "meteor/meteor";
import { Tracker } from "meteor/tracker";
import { Reaction } from "/client/api";
import { Shops, Translations } from "/lib/collections";
import * as Schemas from "/lib/collections/schemas";
import i18next, { packageNamespaces, getLabelsFor, getMessagesFor, i18nextDep } from "./main";

//
// setup options for i18nextBrowserLanguageDetector
// note: this isn't fully operational yet
// language is set by user currently
// progress toward detecting language
// should focus around i18nextBrowserLanguageDetector
//
const options = {
// order and from where user language should be detected
order: ["querystring", "cookie", "localStorage", "navigator", "htmlTag"],

// keys or params to lookup language from
lookupQuerystring: "lng",
lookupCookie: "i18next",
lookupLocalStorage: "i18nextLng",

// cache user language on
caches: ["localStorage", "cookie"],
// optional htmlTag with lang attribute, the default is:
htmlTag: document.documentElement
};


Meteor.startup(() => {
// use tracker autorun to detect language changes
// this only runs on initial page loaded
// and when user.profile.lang updates
Tracker.autorun(function () {
if (Reaction.Subscriptions.Shops.ready() && Meteor.user()) {
const shop = Shops.findOne(Reaction.getShopId());
let language = shop.language;
if (Meteor.user() && Meteor.user().profile && Meteor.user().profile.lang) {
language = Meteor.user().profile.lang;
}
//
// subscribe to user + shop Translations
//
return Meteor.subscribe("Translations", language, () => {
// fetch reaction translations
const translations = Translations.find({}, {
fields: {
_id: 0
}
}).fetch();

// map reduce translations into i18next formatting
const resources = translations.reduce(function (x, y) {
const ns = Object.keys(y.translation)[0];
// first creating the structure, when add additional namespaces
if (x[y.i18n]) {
x[y.i18n][ns] = y.translation[ns];
} else {
x[y.i18n] = y.translation;
}
return x;
}, {});

//
// initialize i18next
//
i18next
.use(i18nextBrowserLanguageDetector)
.use(i18nextLocalStorageCache)
.use(i18nextSprintfPostProcessor)
.use(i18nextJquery)
.init({
detection: options,
debug: false,
ns: packageNamespaces, // translation namespace for every package
defaultNS: "core", // reaction "core" is the default namespace
lng: language, // user session language
fallbackLng: shop ? shop.language : null, // Shop language
resources: resources
// saveMissing: true,
// missingKeyHandler: function (lng, ns, key, fallbackValue) {
// Meteor.call("i18n/addTranslation", lng, ns, key, fallbackValue);
// }
}, (err, t) => {
// someday this should work
// see: https://github.com/aldeed/meteor-simple-schema/issues/494
for (const schema in _.omit(Schemas, "__esModule")) {
if ({}.hasOwnProperty.call(Schemas, schema)) {
const ss = Schemas[schema];
ss.labels(getLabelsFor(ss, schema));
ss.messages(getMessagesFor(ss, schema));
}
}

i18nextDep.changed();

// global first time init event finds and replaces
// data-i18n attributes in html/template source.
$elements = $("[data-i18n]").localize();

// apply language direction to html
if (t("languageDirection") === "rtl") {
return $("html").addClass("rtl");
}
return $("html").removeClass("rtl");
});
}); // return
}
});

//
// init i18nextJquery
//
i18nextJquery.init(i18next, $, {
tName: "t", // --> appends $.t = i18next.t
i18nName: "i18n", // --> appends $.i18n = i18next
handleName: "localize", // --> appends $(selector).localize(opts);
selectorAttr: "data-i18n", // selector for translating elements
targetAttr: "data-i18n-target", // element attribute to grab target element to translate (if diffrent then itself)
parseDefaultValueFromContent: true // parses default values from content ele.val or ele.text
});

// global onRendered event finds and replaces
// data-i18n attributes in html/template source.
// uses methods from i18nextJquery
Template.onRendered(function () {
this.autorun((function () {
return function () {
i18nextDep.depend();
$elements = $("[data-i18n]").localize();
};
})(this));
});
});
2 changes: 1 addition & 1 deletion client/modules/i18n/templates/header/i18n.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<li role="presentation" class="dropdown-header" data-i18n="languages.select">Select Language</li>
<li class="divider"></li>
{{#each languages}}
<li class="{{active}}">
<li class="{{class}}">
<a role="menuitem" class="i18n-language" data-i18n="{{translation}}">{{label}}</a>
</li>
{{/each}}
Expand Down
29 changes: 20 additions & 9 deletions client/modules/i18n/templates/header/i18n.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import { Reaction } from "/client/api";
import { Shops } from "/lib/collections";
import { Session } from "meteor/session";

/**
* i18nChooser helpers
*/

Template.i18nChooser.helpers({
languages() {
const languages = [];
if (Reaction.Subscriptions.Shops.ready()) {
if (Reaction.Subscriptions.Shops.ready() && Meteor.user()) {
const shop = Shops.findOne();
if (typeof shop === "object" && shop.languages) {
for (const language of shop.languages) {
if (language.enabled === true) {
language.translation = "languages." + language.label.toLowerCase();
// appending a helper to let us know this
// language is currently selected
const profile = Meteor.user().profile;
if (profile && profile.lang) {
if (profile.lang === language.i18n) {
language.class = "active";
}
} else if (shop.language === language.i18n) {
// we don't have a profile language
// use the shop default
language.class = "active";
}
languages.push(language);
}
}
Expand All @@ -23,11 +33,7 @@ Template.i18nChooser.helpers({
}
}
}
},
active() {
if (Session.equals("language", this.i18n)) {
return "active";
}
return languages;
}
});

Expand All @@ -38,6 +44,11 @@ Template.i18nChooser.helpers({
Template.i18nChooser.events({
"click .i18n-language"(event) {
event.preventDefault();
return Session.set("language", this.i18n);
//
// this is a sanctioned use of Meteor.user.update
// and only possible because we allow it in the
// UserProfile and ShopMembers publications.
//
Meteor.users.update(Meteor.userId(), {$set: {"profile.lang": this.i18n}});
}
});
1 change: 1 addition & 0 deletions server/publications/collections/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Meteor.publish("UserProfile", function (profileUserId) {
// no need to normal user so see his password hash
const fields = {
"emails": 1,
"profile.lang": 1,
"profile.firstName": 1,
"profile.lastName": 1,
"profile.familyName": 1,
Expand Down
1 change: 1 addition & 0 deletions server/publications/collections/members.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Meteor.publish("ShopMembers", function () {
emails: 1,
username: 1,
roles: 1,
"profile.lang": 1,
"services.google.name": 1,
"services.google.email": 1,
"services.google.picture": 1,
Expand Down
Loading