Bug 1450781 - Enable pseudolocalization in Fluent. draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Thu, 26 Apr 2018 13:30:12 -0700
changeset 793379 20358fdad4fe0302226bac521fa9d5682d6900b2
parent 792573 7c83ceac4be6d055bebd870a82b78b76de14b9d7
push id109361
push userbmo:gandalf@aviary.pl
push dateWed, 09 May 2018 22:16:52 +0000
bugs1450781
milestone62.0a1
Bug 1450781 - Enable pseudolocalization in Fluent. MozReview-Commit-ID: 4vrmLuto1CQ
intl/l10n/L10nRegistry.jsm
intl/l10n/Localization.jsm
intl/l10n/MessageContext.jsm
--- a/intl/l10n/L10nRegistry.jsm
+++ b/intl/l10n/L10nRegistry.jsm
@@ -239,16 +239,62 @@ const MSG_CONTEXT_OPTIONS = {
           return "macos";
         default:
           return "other";
       }
     }
   }
 };
 
+const ACCENTED_MAP = {
+      // ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ
+      "caps": [550, 385, 391, 7698, 7702, 401, 403, 294, 298, 308, 310, 319, 7742, 544, 510, 420, 586, 344, 350, 358, 364, 7804, 7814, 7818, 7822, 7824],
+      // ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ
+      "small": [551, 384, 392, 7699, 7703, 402, 608, 295, 299, 309, 311, 320, 7743, 414, 511, 421, 587, 345, 351, 359, 365, 7805, 7815, 7819, 7823, 7825],
+};
+
+const FLIPPED_MAP = {
+      // ∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅMX⅄Z
+      "caps": [8704, 1296, 8579, 5601, 398, 8498, 8513, 72, 73, 383, 1276, 8514, 87, 78, 79, 1280, 210, 7450, 83, 8869, 8745, 581, 77, 88, 8516, 90],
+      // ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz
+      "small": [592, 113, 596, 112, 477, 607, 387, 613, 305, 638, 670, 645, 623, 117, 111, 100, 98, 633, 115, 647, 110, 652, 653, 120, 654, 122],
+};
+
+function transformString(map, prefix = "", postfix = "", msg) {
+  // XML entities (&#x202a;) and XML tags.
+  const reExcluded = /(&[#\w]+;|<\s*.+?\s*>)/;
+
+  const parts = msg.split(reExcluded);
+  const modified = parts.map((part) => {
+    if (reExcluded.test(part)) {
+      return part;
+    }
+    return prefix + part.replace(/[a-z]/ig, (ch) => {
+      let cc = ch.charCodeAt(0);
+      if (cc >= 97 && cc <= 122) {
+        if (ch === "a" || ch === "e" || ch === "o" || ch === "u") {
+          const newChar = String.fromCodePoint(map.small[cc - 97]);
+          return newChar + newChar;
+        }
+        return String.fromCodePoint(map.small[cc - 97]);
+      }
+      if (cc >= 65 && cc <= 90) {
+        return String.fromCodePoint(map.caps[cc - 65]);
+      }
+      return ch;
+    }) + postfix;
+  });
+  return modified.join("");
+}
+
+const PSEUDO_STRATEGIES = {
+  "accented": transformString.bind(null, ACCENTED_MAP, "", ""),
+  "bidi": transformString.bind(null, FLIPPED_MAP, "\u202e", "\u202c"),
+};
+
 /**
  * Generates a single MessageContext by loading all resources
  * from the listed sources for a given locale.
  *
  * The function casts all error cases into a Promise that resolves with
  * value `null`.
  * This allows the caller to be an async generator without using
  * try/catch clauses.
@@ -265,17 +311,21 @@ function generateContext(locale, sources
   }
 
   const fetchPromises = resourceIds.map((resourceId, i) => {
     return L10nRegistry.sources.get(sourcesOrder[i]).fetchFile(locale, resourceId);
   });
 
   const ctxPromise = Promise.all(fetchPromises).then(
     dataSets => {
-      const ctx = new MessageContext(locale, MSG_CONTEXT_OPTIONS);
+      const pseudoNameFromPref = Services.prefs.getStringPref("intl.l10n.pseudo", "");
+      const ctx = new MessageContext(locale, {
+        ...MSG_CONTEXT_OPTIONS,
+        transform: PSEUDO_STRATEGIES[pseudoNameFromPref],
+      });
       for (const data of dataSets) {
         if (data === null) {
           return null;
         }
         ctx.addMessages(data);
       }
       return ctx;
     },
--- a/intl/l10n/Localization.jsm
+++ b/intl/l10n/Localization.jsm
@@ -243,30 +243,38 @@ class Localization {
     return val;
   }
 
   /**
    * Register weak observers on events that will trigger cache invalidation
    */
   registerObservers() {
     Services.obs.addObserver(this, "intl:app-locales-changed", true);
+    Services.prefs.addObserver("intl.l10n.pseudo", this, true);
   }
 
   /**
    * Default observer handler method.
    *
    * @param {String} subject
    * @param {String} topic
    * @param {Object} data
    */
   observe(subject, topic, data) {
     switch (topic) {
       case "intl:app-locales-changed":
         this.onLanguageChange();
         break;
+      case "nsPref:changed":
+        switch (data) {
+          case "intl.l10n.pseudo":
+            L10nRegistry.ctxCache.clear();
+            this.onLanguageChange();
+        }
+        break;
       default:
         break;
     }
   }
 
   /**
    * This method should be called when there's a reason to believe
    * that language negotiation or available resources changed.
--- a/intl/l10n/MessageContext.jsm
+++ b/intl/l10n/MessageContext.jsm
@@ -1708,23 +1708,24 @@ class MessageContext {
    *
    *   - `useIsolating` - boolean specifying whether to use Unicode isolation
    *                    marks (FSI, PDI) for bidi interpolations.
    *
    * @param   {string|Array<string>} locales - Locale or locales of the context
    * @param   {Object} [options]
    * @returns {MessageContext}
    */
-  constructor(locales, { functions = {}, useIsolating = true } = {}) {
+  constructor(locales, { functions = {}, useIsolating = true, transform = undefined } = {}) {
     this.locales = Array.isArray(locales) ? locales : [locales];
 
     this._terms = new Map();
     this._messages = new Map();
     this._functions = functions;
     this._useIsolating = useIsolating;
+    this._transform = transform;
     this._intls = new WeakMap();
   }
 
   /*
    * Return an iterator over public `[id, message]` pairs.
    *
    * @returns {Iterator}
    */
@@ -1824,22 +1825,22 @@ class MessageContext {
    * @param   {Object | string}    message
    * @param   {Object | undefined} args
    * @param   {Array}              errors
    * @returns {?string}
    */
   format(message, args, errors) {
     // optimize entities which are simple strings with no attributes
     if (typeof message === "string") {
-      return message;
+      return this._transform ? this._transform(message) : message;
     }
 
     // optimize simple-string entities with attributes
     if (typeof message.val === "string") {
-      return message.val;
+      return this._transform ? this._transform(message.val) : message.val;
     }
 
     // optimize entities with null values
     if (message.val === undefined) {
       return null;
     }
 
     return resolve(this, args, message, errors);