locales/Contextualizer.js

/* globals window, localStorage, navigator */
var FieldDBObject = require("./../FieldDBObject").FieldDBObject;
var ELanguages = require("./ELanguages").ELanguages;
var CORS = require("./../CORS").CORS;
var Q = require("q");

var english_texts = require("./en/messages.json");
var spanish_texts = require("./es/messages.json");
var elanguages = require("./elanguages.json");

/**
 * @class The contextualizer can resolves strings depending on context and locale of the user
 *  @name  Contextualizer
 *
 * @property {ELanguage} defaultLocale The language/context to use if a translation/contextualization is missing.
 * @property {ELanguage} currentLocale The current locale to use (often the browsers default locale, or a corpus" default locale).
 *
 * @extends FieldDBObject
 * @constructs
 */
var Contextualizer = function Contextualizer(options) {
  if (!this._fieldDBtype) {
    this._fieldDBtype = "Contextualizer";
  }
  this.debug("Constructing Contextualizer ", options);
  // this.debugMode = true;
  var localArguments = arguments;
  if (!options) {
    options = {};
    localArguments = [options];
  }
  if (!options.defaultLocale || !options.defaultLocale.iso) {
    options.defaultLocale = {
      iso: "en"
    };
  }
  // if (!options.currentLocale || !options.currentLocale.iso) {
  //   options.currentLocale = {
  //     iso: "en"
  //   };
  // }
  if (!options.currentContext) {
    options.currentContext = "default";
  }
  if (!options.elanguages) {
    options.elanguages = elanguages;
  }
  FieldDBObject.apply(this, localArguments);
  if (!options || options.alwaysConfirmOkay === undefined) {
    this.debug("By default it will be okay for users to modify global locale strings. IF they are saved this will affect other users.");
    this.alwaysConfirmOkay = true;
  }
  if (this.userOverridenLocalePreference) {
    this.currentLocale = this.userOverridenLocalePreference;
  } else {
    try {
      if (navigator.languages[0].indexOf(this.currentLocale.iso) === -1) {
        this.currentLocale = navigator.languages[0];
      }
    } catch (e) {
      this.debug("not using hte browser's language", e);
    }
  }
  return this;
};

Contextualizer.prototype = Object.create(FieldDBObject.prototype, /** @lends Contextualizer.prototype */ {
  constructor: {
    value: Contextualizer
  },

  INTERNAL_MODELS: {
    value: {
      elanguages: ELanguages
    }
  },

  _require: {
    value: (typeof global !== "undefined") ? global.require : (typeof window !== "undefined") ? window.require : null
  },

  data: {
    get: function() {
      return this._data;
    },
    set: function(value) {
      this._data = value;
    }
  },

  currentLocale: {
    get: function() {
      if (this._currentLocale) {
        return this._currentLocale;
      }
      if (this._mostAvailableLanguage) {
        return this._mostAvailableLanguage;
      }
      return this.defaultLocale;
    },
    set: function(value) {
      if (value === this._currentLocale) {
        return;
      }

      if (value && value.toLowerCase && typeof value === "string") {
        value = value.toLowerCase().replace(/[^a-z-]/g, "");
        if (this.elanguages && this.elanguages[value]) {
          value = this.elanguages[value];
        } else {
          value = {
            iso: value
          };
        }
      }

      this.debug("SETTING LOCALE FROM " + this._currentLocale + " to ", value, this.data);
      this._currentLocale = value;
    }
  },

  userOverridenLocalePreference: {
    get: function() {
      var userOverridenLocalePreference;
      try {
        userOverridenLocalePreference = JSON.parse(localStorage.getItem("_userOverridenLocalePreference"));
      } catch (e) {
        this.debug("Localstorage is not available, using the object there will be no persistance across loads", e, this._userOverridenLocalePreference);
        userOverridenLocalePreference = this._userOverridenLocalePreference;
      }
      if (!userOverridenLocalePreference) {
        return;
      }
      return userOverridenLocalePreference;
    },
    set: function(value) {
      if (value) {
        try {
          localStorage.setItem("_userOverridenLocalePreference", JSON.stringify(value));
        } catch (e) {
          this._userOverridenLocalePreference = value;
          this.debug("Localstorage is not available, using the object there will be no persistance across loads", e, this._userOverridenLocalePreference);
        }
      } else {
        try {
          localStorage.removeItem("_userOverridenLocalePreference");
        } catch (e) {
          this.debug("Localstorage is not available, using the object there will be no persistance across loads", e, this._userOverridenLocalePreference);
          delete this._userOverridenLocalePreference;
        }
      }
    }
  },

  availableLanguages: {
    get: function() {
      this.data = this.data || {};
      if (this._availableLanguages && this.data[this._availableLanguages._collection[0].iso] && this._availableLanguages._collection[0].length === this.data[this._availableLanguages._collection[0].iso].length) {
        return this._availableLanguages;
      }
      var availLanguages = new ELanguages(),
        bestAvailabilityCount = 0;

      for (var code in this.data) {
        if (this.data.hasOwnProperty(code)) {
          this.elanguages[code].length = this.data[code].length;
          if (this.elanguages[code].length > bestAvailabilityCount) {
            availLanguages.unshift(this.elanguages[code]);
            bestAvailabilityCount = this.elanguages[code].length;
          } else {
            availLanguages.push(this.elanguages[code]);
          }
        }
      }
      if (bestAvailabilityCount === 0 || availLanguages.length === 0) {
        this.todo("Ensuring that at least english is an available language, not sure if this is a good idea.");
        availLanguages.unshift(this.elanguages.en);
      } else {
        availLanguages._collection.map(function(language) {
          language.percentageOfAvailability = Math.round(language.length / bestAvailabilityCount * 100);
          return language;
        });
      }
      this.todo("test whether setting the currentLocale to the most complete locale has adverse affects.");
      this._mostAvailableLanguage = availLanguages._collection[0];
      this._availableLanguages = availLanguages;
      return availLanguages;
    }
  },

  defaults: {
    get: function() {
      return {
        en: JSON.parse(JSON.stringify(english_texts)),
        es: JSON.parse(JSON.stringify(spanish_texts))
      };
    }
  },

  loadDefaults: {
    value: function() {
      if (this.defaults.en) {
        this.addMessagesToContextualizedStrings("en", this.defaults.en);
      } else {
        this.debug("English Locales did not load.");
      }
      if (this.defaults.es) {
        this.addMessagesToContextualizedStrings("es", this.defaults.es);
      } else {
        this.debug("English Locales did not load.");
      }
      return this;
    }
  },

  localize: {
    value: function(message, optionalLocaleForThisCall) {
      return this.contextualize(message, optionalLocaleForThisCall);
    }
  },

  contextualize: {
    value: function(message, optionalLocaleForThisCall) {
      if (!optionalLocaleForThisCall) {
        optionalLocaleForThisCall = this.currentLocale.iso;
      }
      if (optionalLocaleForThisCall && optionalLocaleForThisCall.iso) {
        optionalLocaleForThisCall = optionalLocaleForThisCall.iso;
      }
      this.debug("Resolving localization in " + optionalLocaleForThisCall);
      var result = message,
        aproperty;

      // Use the current context if the caller is requesting localization of an object
      if (typeof message === "object") {
        var foundAContext = false;
        for (aproperty in message) {
          if (!message.hasOwnProperty(aproperty)) {
            continue;
          }
          if (aproperty.indexOf(this.currentContext) > -1 || ((this.currentContext === "child" || this.currentContext === "game") && aproperty.indexOf("gamified") > -1)) {
            result = message[aproperty];
            foundAContext = true;
            this.debug("Using " + aproperty + " for this contxtualization.");
            break;
          }
        }
        if (!foundAContext && message.default) {
          this.debug("Using default for this contxtualization. ", message);
          result = message.default;
        }
      }

      if (!this.data) {
        this.warn("No localizations available, resolving the key itself: ", result);
        return result;
      }

      var keepTrying = true;
      if (this.data[optionalLocaleForThisCall] && this.data[optionalLocaleForThisCall][result] && this.data[optionalLocaleForThisCall][result].message !== undefined) {
        result = this.data[optionalLocaleForThisCall][result].message;
        this.debug("Resolving requested contextualization using requested language: ", result);
        keepTrying = false;
      } else if (this.data[this.defaultLocale.iso] && this.data[this.defaultLocale.iso][result] && this.data[this.defaultLocale.iso][result].message !== undefined) {
        result = this.data[this.defaultLocale.iso][result].message;
        this.debug("Resolving requested contextualization using default language: ", result);
        keepTrying = false;
      } else {
        if (typeof message === "object") {
          if (message[result] && this.data[optionalLocaleForThisCall] && this.data[optionalLocaleForThisCall][message[result]] && this.data[optionalLocaleForThisCall][message[result]].message !== undefined && this.data[optionalLocaleForThisCall][message[result]].message) {
            result = this.data[optionalLocaleForThisCall][message[result]].message;
            this.debug("Resolving localization using requested contextualization: ", message[result]);
            keepTrying = false;
          } else if (message.default && this.data[optionalLocaleForThisCall] && this.data[optionalLocaleForThisCall][message.default] && this.data[optionalLocaleForThisCall][message.default].message !== undefined && this.data[optionalLocaleForThisCall][message.default].message) {
            result = this.data[optionalLocaleForThisCall][message.default].message;
            this.debug("Resolving localization using default contextualization: ", message.default);
            keepTrying = false;
          } else if (message.default && this.data[this.defaultLocale.iso] && this.data[this.defaultLocale.iso][message.default] && this.data[this.defaultLocale.iso][message.default].message !== undefined && this.data[this.defaultLocale.iso][message.default].message) {
            result = this.data[this.defaultLocale.iso][message.default].message;
            this.debug("Resolving localization using default contextualization and default locale: ", message.default);
            keepTrying = false;
          }
        }
        if (keepTrying && this.data[this.defaultLocale.iso] && this.data[this.defaultLocale.iso][result] && this.data[this.defaultLocale.iso][result].message !== undefined && this.data[this.defaultLocale.iso][result].message) {
          result = this.data[this.defaultLocale.iso][result].message;
          this.debug("Resolving localization using default: ", result);
        }
      }

      if (keepTrying && !this.requestedCorpusSpecificLocalizations && FieldDBObject && FieldDBObject.application && FieldDBObject.application.corpus && FieldDBObject.application.corpus.loaded && typeof FieldDBObject.application.corpus.getCorpusSpecificLocalizations === "function") {
        FieldDBObject.application.corpus.getCorpusSpecificLocalizations();
        this.requestedCorpusSpecificLocalizations = true;
      }
      result = result.replace(/^locale_/, "");
      return result;
    }
  },

  /**
   *
   * @param  {String} key   A locale to save the message to
   * @param  {String} value a message which should replace the existing localization
   * @return {Promise}       A promise for whether or not the update was confirmed and executed
   */
  updateContextualization: {
    value: function(key, value) {
      var deferred = Q.defer(),
        self = this,
        previousMessage = "",
        verb = "create ";

      this.whenReadys = this.whenReadys || [];

      this.todo("Test async updateContextualization");
      this.whenReadys.push(deferred.promise);

      this.data[this.currentLocale.iso] = this.data[this.currentLocale.iso] || {};
      if (this.data[this.currentLocale.iso][key] && this.data[this.currentLocale.iso][key].message === value) {
        Q.nextTick(function() {
          deferred.resolve(value);
        });
        return deferred.promise; //no change
      }

      if (this.data[this.currentLocale.iso][key]) {
        previousMessage = this.data[this.currentLocale.iso][key].message;
        verb = "update ";
      }

      var addTheUsersMessage = function(addkey, addvalue) {
        self.debug("Adding the user's message. ", addkey, addvalue);
        self.data[self.currentLocale.iso][addkey] = self.data[self.currentLocale.iso][addkey] || {};
        self.data[self.currentLocale.iso][addkey].message = addvalue;

        if (!self.fossil) {
          self.fossil = self.toJSON();
        }
        self.unsaved = true;
        var newLocaleItem = {};
        newLocaleItem[key] = {
          message: addvalue
        };
        self.addMessagesToContextualizedStrings(self.currentLocale.iso, newLocaleItem);
        Q.nextTick(function() {
          deferred.resolve(addvalue);
        });
      };

      if (!this.testingAsyncConfirm && this.alwaysConfirmOkay /* run synchonosuly whenever possible */ ) {
        self.debug("  Running synchonosuly. ", key, value);
        addTheUsersMessage(key, value);
        return deferred.promise;
      }

      self.debug("     Running asynchonosuly. ", key, value);
      this.confirm("Do you also want to " + verb + key + " for other users? \n" + previousMessage + " -> " + value)
        .then(function(response) {
            self.debug("Recieved confirmation ,", response);
            addTheUsersMessage(key, value);
          },
          function(reason) {
            self.debug("Not updating , user clicked cancel", reason);
            deferred.reject(reason);
          })
        .fail(
          function(error) {
            console.error(error.stack, self);
            deferred.reject(error);
          });

      return deferred.promise;
    }
  },

  audio: {
    value: function(key) {
      this.debug("Resolving localization in " + this.currentLocale.iso);
      var result = {};
      if (!this.data) {
        this.warn("No localizations available, resolving empty audio details");
        return result;
      }

      if (this.data[this.currentLocale.iso] && this.data[this.currentLocale.iso][key] && this.data[this.currentLocale.iso][key].audio !== undefined && this.data[this.currentLocale.iso][key].audio) {
        result = this.data[this.currentLocale.iso][key].audio;
        this.debug("Resolving localization audio using requested language: ", result);
      } else {
        if (this.data[this.defaultLocale.iso] && this.data[this.defaultLocale.iso][key] && this.data[this.defaultLocale.iso][key].audio !== undefined && this.data[this.defaultLocale.iso][key].audio) {
          result = this.data[this.defaultLocale.iso][key].audio;
          this.warn("Resolving localization audio using default: ", result);
        }
      }
      return result;
    }
  },

  addUrls: {
    value: function(files, baseUrl) {
      var promises = [],
        f;

      for (f = 0; f < files.length; f++) {
        promises.push(this.addUrl(files[f], baseUrl));
      }
      return Q.all(promises);
    }
  },

  addUrl: {
    value: function(file, baseUrl) {
      var deferred = Q.defer(),
        localeCode,
        self = this;

      if (!baseUrl && FieldDBObject && FieldDBObject.application && FieldDBObject.application.corpus && FieldDBObject.application.corpus.url) {
        this.debug("using corpus as base url");
        baseUrl = FieldDBObject.application.corpus.url;
      }

      if (file.indexOf("/messages.json" > -1)) {
        localeCode = file.replace("/messages.json", "");
        if (localeCode.indexOf("/") > -1) {
          localeCode = localeCode.substring(localeCode.lastIndexOf("/"));
        }
        localeCode = localeCode.replace(/[^a-zA-Z-]/g, "").toLowerCase();
        if (!localeCode || localeCode.length < 2) {
          localeCode = "default";
        }
      } else {
        localeCode = "en";
      }

      CORS.makeCORSRequest({
        method: "GET",
        url: baseUrl + "/" + file,
        dataType: "json"
      }).then(function(localeMessages) {
        self.originalDocs = self.originalDocs || [];
        self.originalDocs.push(file);
        self.addMessagesToContextualizedStrings(localeCode, localeMessages)
          .then(deferred.resolve,
            deferred.reject)
          .fail(function(error) {
            console.error(error.stack, self);
            deferred.reject(error);
          });
      }, function(error) {
        self.warn("There werent any locales at this url" + baseUrl + " :( Maybe this database has no custom locale messages.", error);
      }).fail(function(error) {
        console.error(error.stack, self);
        deferred.reject(error);
      });

      return deferred.promise;
    }
  },

  addMessagesToContextualizedStrings: {
    value: function(localeCode, localeData) {
      var deferred = Q.defer(),
        self = this;

      if (!localeData) {
        Q.nextTick(function() {
          deferred.reject("The locales data was empty!");
        });
        return deferred.promise;
      }

      if (!localeCode && localeData._id) {
        localeCode = localeData._id.replace("/messages.json", "");
        if (localeCode.indexOf("/") > -1) {
          localeCode = localeCode.substring(localeCode.lastIndexOf("/"));
        }
        localeCode = localeCode.replace(/[^a-zA-Z-]/g, "").toLowerCase();
        if (!localeCode || localeCode.length < 2) {
          localeCode = "default";
        }
      }
      self.originalDocs = self.originalDocs || [];
      self.originalDocs.push(localeData);

      self.data = self.data || {};
      for (var message in localeData) {
        if (localeData.hasOwnProperty(message) && message.indexOf("_") !== 0) {
          self.data[localeCode] = self.data[localeCode] || {
            length: 0
          };
          self.data[localeCode][message] = localeData[message];
          self.data[localeCode].length++;
        }
      }

      Q.nextTick(function() {
        deferred.resolve(self.data);
      });
      return deferred.promise;
    }
  },

  save: {
    value: function() {
      var promises = [];
      for (var locale in this.data) {
        if (!this.data.hasOwnProperty(locale)) {
          continue;
        }

        this.debug("Requsting save of " + locale);
        var doc = new FieldDBObject(this.data[locale]);
        doc.dbname = this.dbname;
        this.debug("Will save locale save of ", doc);

        var userHasModifiedContexualizations = !!this.fossil;

        if (this.email) {
          this.debug(" via Git ", doc);
          promises.push(doc.saveToGit({
            email: this.email,
            message: "Updated locale messages"
          }, userHasModifiedContexualizations));
        } else {
          doc.id = locale + "/messages.json";
          // doc.debugMode = true;
          this.debug("   via REST ", doc);
          promises.push(doc.save(null, userHasModifiedContexualizations));
        }

      }
      return Q.allSettled(promises);
    }
  }

});

exports.Contextualizer = Contextualizer;