Bug 1339650 - Introduce mozIntl.DateTimeFormat. r=jfkthame draft
authorZibi Braniecki <gandalf@mozilla.com>
Thu, 02 Mar 2017 16:39:17 -0800
changeset 553748 2b319acbb9fe1c7bdb3bc555baff8b9030946715
parent 552691 272ce6c2572164f5f6a9fba2a980ba9ccf50770c
child 622170 0d6fc92e1ca1c0b5c7e4c10ddb82438c6e116466
push id51751
push userzbraniecki@mozilla.com
push dateThu, 30 Mar 2017 15:49:35 +0000
reviewersjfkthame
bugs1339650
milestone55.0a1
Bug 1339650 - Introduce mozIntl.DateTimeFormat. r=jfkthame MozReview-Commit-ID: 1jnit4IlDN6
intl/locale/LocaleService.cpp
intl/locale/LocaleService.h
intl/locale/OSPreferences.cpp
intl/locale/mac/OSPreferences_mac.cpp
intl/locale/windows/OSPreferences_win.cpp
toolkit/components/mozintl/MozIntlHelper.cpp
toolkit/components/mozintl/mozIMozIntl.idl
toolkit/components/mozintl/mozIMozIntlHelper.idl
toolkit/components/mozintl/mozIntl.js
toolkit/components/mozintl/test/test_mozintl.js
toolkit/components/mozintl/test/test_mozintlhelper.js
--- a/intl/locale/LocaleService.cpp
+++ b/intl/locale/LocaleService.cpp
@@ -439,16 +439,23 @@ LocaleService::Observe(nsISupports *aSub
     nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
     if (obs) {
       obs->NotifyObservers(nullptr, "intl:requested-locales-changed", nullptr);
     }
   }
   return NS_OK;
 }
 
+bool
+LocaleService::LanguagesMatch(const nsCString& aRequested,
+                              const nsCString& aAvailable)
+{
+  return Locale(aRequested, true).LanguageMatches(Locale(aAvailable, true));
+}
+
 /**
  * mozILocaleService methods
  */
 
 static char**
 CreateOutArray(const nsTArray<nsCString>& aArray)
 {
   uint32_t n = aArray.Length();
@@ -643,30 +650,38 @@ LocaleService::Locale::Locale(const nsCS
       mRegion.Assign(NS_LITERAL_CSTRING("*"));
     }
     if (mVariant.IsEmpty()) {
       mVariant.Assign(NS_LITERAL_CSTRING("*"));
     }
   }
 }
 
+static bool
+SubtagMatches(const nsCString& aSubtag1, const nsCString& aSubtag2)
+{
+  return aSubtag1.EqualsLiteral("*") ||
+         aSubtag2.EqualsLiteral("*") ||
+         aSubtag1.Equals(aSubtag2, nsCaseInsensitiveCStringComparator());
+}
+
 bool
 LocaleService::Locale::Matches(const LocaleService::Locale& aLocale) const
 {
-  auto subtagMatches = [](const nsCString& aSubtag1,
-                          const nsCString& aSubtag2) {
-    return aSubtag1.EqualsLiteral("*") ||
-           aSubtag2.EqualsLiteral("*") ||
-           aSubtag1.Equals(aSubtag2, nsCaseInsensitiveCStringComparator());
-  };
+  return SubtagMatches(mLanguage, aLocale.mLanguage) &&
+         SubtagMatches(mScript, aLocale.mScript) &&
+         SubtagMatches(mRegion, aLocale.mRegion) &&
+         SubtagMatches(mVariant, aLocale.mVariant);
+}
 
-  return subtagMatches(mLanguage, aLocale.mLanguage) &&
-         subtagMatches(mScript, aLocale.mScript) &&
-         subtagMatches(mRegion, aLocale.mRegion) &&
-         subtagMatches(mVariant, aLocale.mVariant);
+bool
+LocaleService::Locale::LanguageMatches(const LocaleService::Locale& aLocale) const
+{
+  return SubtagMatches(mLanguage, aLocale.mLanguage) &&
+         SubtagMatches(mScript, aLocale.mScript);
 }
 
 void
 LocaleService::Locale::SetVariantRange()
 {
   mVariant.AssignLiteral("*");
 }
 
--- a/intl/locale/LocaleService.h
+++ b/intl/locale/LocaleService.h
@@ -167,30 +167,34 @@ public:
                           LangNegStrategy aLangNegStrategy,
                           nsTArray<nsCString>& aRetVal);
 
   /**
    * Returns whether the current app locale is RTL.
    */
   bool IsAppLocaleRTL();
 
+  static bool LanguagesMatch(const nsCString& aRequested,
+                             const nsCString& aAvailable);
+
 private:
   /**
    * Locale object, a BCP47-style tag decomposed into subtags for
    * matching purposes.
    *
    * If constructed with aRange = true, any missing subtags will be
    * set to "*".
    */
   class Locale
   {
   public:
     Locale(const nsCString& aLocale, bool aRange);
 
     bool Matches(const Locale& aLocale) const;
+    bool LanguageMatches(const Locale& aLocale) const;
 
     void SetVariantRange();
     void SetRegionRange();
 
     bool AddLikelySubtags(); // returns false if nothing changed
 
     const nsCString& AsString() const {
       return mLocaleStr;
--- a/intl/locale/OSPreferences.cpp
+++ b/intl/locale/OSPreferences.cpp
@@ -145,19 +145,26 @@ OSPreferences::GetDateTimePatternForStyl
     case DateTimeFormatStyle::Invalid:
       dateStyle = UDAT_NONE;
       break;
   }
 
   const int32_t kPatternMax = 160;
   UChar pattern[kPatternMax];
 
+  nsAutoCString locale;
+  if (aLocale.IsEmpty()) {
+    LocaleService::GetInstance()->GetAppLocaleAsBCP47(locale);
+  } else {
+    locale.Assign(aLocale);
+  }
+
   UErrorCode status = U_ZERO_ERROR;
   UDateFormat* df = udat_open(timeStyle, dateStyle,
-                              PromiseFlatCString(aLocale).get(),
+                              locale.get(),
                               nullptr, -1, nullptr, -1, &status);
   if (U_FAILURE(status)) {
     return false;
   }
 
   int32_t patsize = udat_toPattern(df, false, pattern, kPatternMax, &status);
   udat_close(df);
   if (U_FAILURE(status)) {
--- a/intl/locale/mac/OSPreferences_mac.cpp
+++ b/intl/locale/mac/OSPreferences_mac.cpp
@@ -1,15 +1,16 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  *
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "OSPreferences.h"
+#include "mozilla/intl/LocaleService.h"
 #include <Carbon/Carbon.h>
 
 using namespace mozilla::intl;
 
 bool
 OSPreferences::ReadSystemLocales(nsTArray<nsCString>& aLocaleList)
 {
   MOZ_ASSERT(aLocaleList.IsEmpty());
@@ -62,23 +63,36 @@ ToCFDateFormatterStyle(OSPreferences::Da
 // Given an 8-bit Gecko string, create a corresponding CFLocale;
 // if aLocale is empty, returns a copy of the system's current locale.
 // May return null on failure.
 // Follows Core Foundation's Create rule, so the caller is responsible to
 // release the returned reference.
 static CFLocaleRef
 CreateCFLocaleFor(const nsACString& aLocale)
 {
+  nsAutoCString reqLocale;
+  nsAutoCString systemLocale;
+
+  OSPreferences::GetInstance()->GetSystemLocale(systemLocale);
+
   if (aLocale.IsEmpty()) {
-    return CFLocaleCopyCurrent();
+    LocaleService::GetInstance()->GetAppLocaleAsBCP47(reqLocale);
+  } else {
+    reqLocale.Assign(aLocale);
   }
+
+  bool match = LocaleService::LanguagesMatch(reqLocale, systemLocale);
+  if (match) {
+    return ::CFLocaleCopyCurrent();
+  }
+
   CFStringRef identifier =
     CFStringCreateWithBytesNoCopy(kCFAllocatorDefault,
-                                  (const uint8_t*)aLocale.BeginReading(),
-                                  aLocale.Length(), kCFStringEncodingASCII,
+                                  (const uint8_t*)reqLocale.BeginReading(),
+                                  reqLocale.Length(), kCFStringEncodingASCII,
                                   false, kCFAllocatorNull);
   if (!identifier) {
     return nullptr;
   }
   CFLocaleRef locale = CFLocaleCreate(kCFAllocatorDefault, identifier);
   CFRelease(identifier);
   return locale;
 }
--- a/intl/locale/windows/OSPreferences_win.cpp
+++ b/intl/locale/windows/OSPreferences_win.cpp
@@ -1,16 +1,18 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  *
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "OSPreferences.h"
+#include "mozilla/intl/LocaleService.h"
 #include "nsWin32Locale.h"
+#include "nsReadableUtils.h"
 
 using namespace mozilla::intl;
 
 bool
 OSPreferences::ReadSystemLocales(nsTArray<nsCString>& aLocaleList)
 {
   MOZ_ASSERT(aLocaleList.IsEmpty());
 
@@ -65,16 +67,38 @@ ToTimeLCType(OSPreferences::DateTimeForm
       return LOCALE_STIMEFORMAT;
     case OSPreferences::DateTimeFormatStyle::Invalid:
     default:
       MOZ_ASSERT_UNREACHABLE("invalid time format");
       return LOCALE_STIMEFORMAT;
   }
 }
 
+LPWSTR
+GetWindowsLocaleFor(const nsACString& aLocale, LPWSTR aBuffer)
+{
+  nsAutoCString reqLocale;
+  nsAutoCString systemLocale;
+  OSPreferences::GetInstance()->GetSystemLocale(systemLocale);
+
+  if (aLocale.IsEmpty()) {
+    LocaleService::GetInstance()->GetAppLocaleAsBCP47(reqLocale);
+  } else {
+    reqLocale.Assign(aLocale);
+  }
+
+  bool match = LocaleService::LanguagesMatch(reqLocale, systemLocale);
+  if (match || reqLocale.Length() >= LOCALE_NAME_MAX_LENGTH) {
+    return LOCALE_NAME_USER_DEFAULT;
+  }
+
+  UTF8ToUnicodeBuffer(reqLocale, (char16_t*)aBuffer);
+  return aBuffer;
+}
+
 /**
  * Windows API includes regional preferences from the user only
  * if we pass empty locale string or if the locale string matches
  * the current locale.
  *
  * Since Windows API only allows us to retrieve two options - short/long
  * we map it to our four options as:
  *
@@ -87,37 +111,34 @@ ToTimeLCType(OSPreferences::DateTimeForm
  * for combined date/time string, since Windows API does not provide an
  * option for this.
  */
 bool
 OSPreferences::ReadDateTimePattern(DateTimeFormatStyle aDateStyle,
                                    DateTimeFormatStyle aTimeStyle,
                                    const nsACString& aLocale, nsAString& aRetVal)
 {
-  LPWSTR localeName = LOCALE_NAME_USER_DEFAULT;
-  nsAutoString localeNameBuffer;
-  if (!aLocale.IsEmpty()) {
-    localeNameBuffer.AppendASCII(aLocale.BeginReading(), aLocale.Length());
-    localeName = (LPWSTR)localeNameBuffer.BeginReading();
-  }
+  WCHAR buffer[LOCALE_NAME_MAX_LENGTH];
+
+  LPWSTR localeName = GetWindowsLocaleFor(aLocale, buffer);
 
   bool isDate = aDateStyle != DateTimeFormatStyle::None &&
                 aDateStyle != DateTimeFormatStyle::Invalid;
   bool isTime = aTimeStyle != DateTimeFormatStyle::None &&
                 aTimeStyle != DateTimeFormatStyle::Invalid;
 
   // If both date and time are wanted, we'll initially read them into a
   // local string, and then insert them into the overall date+time pattern;
   // but if only one is needed we'll work directly with the return value.
   // Set 'str' to point to the string we will use to retrieve patterns
   // from Windows.
   nsAutoString tmpStr;
   nsAString* str;
   if (isDate && isTime) {
-    if (!GetDateTimeConnectorPattern(aLocale, aRetVal)) {
+    if (!GetDateTimeConnectorPattern(NS_ConvertUTF16toUTF8(localeName), aRetVal)) {
       NS_WARNING("failed to get date/time connector");
       aRetVal.AssignLiteral(u"{1} {0}");
     }
     str = &tmpStr;
   } else if (isDate || isTime) {
     str = &aRetVal;
   } else {
     aRetVal.Truncate(0);
--- a/toolkit/components/mozintl/MozIntlHelper.cpp
+++ b/toolkit/components/mozintl/MozIntlHelper.cpp
@@ -78,16 +78,37 @@ MozIntlHelper::AddPluralRulesConstructor
   if (!js::AddPluralRulesConstructor(cx, realIntlObj)) {
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
+MozIntlHelper::AddDateTimeFormatConstructor(JS::Handle<JS::Value> val, JSContext* cx)
+{
+  if (!val.isObject()) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  JS::Rooted<JSObject*> realIntlObj(cx, js::CheckedUnwrap(&val.toObject()));
+  if (!realIntlObj) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  JSAutoCompartment ac(cx, realIntlObj);
+
+  if (!js::AddMozDateTimeFormatConstructor(cx, realIntlObj)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 MozIntlHelper::AddGetLocaleInfo(JS::Handle<JS::Value> val, JSContext* cx)
 {
   static const JSFunctionSpec funcs[] = {
     JS_SELF_HOSTED_FN("getLocaleInfo", "Intl_getLocaleInfo", 1, 0),
     JS_FS_END
   };
 
   return AddFunctions(cx, val, funcs);
--- a/toolkit/components/mozintl/mozIMozIntl.idl
+++ b/toolkit/components/mozintl/mozIMozIntl.idl
@@ -37,9 +37,10 @@
 [scriptable, uuid(7f63279a-1a29-4ae6-9e7a-dc9684a23530)]
 interface mozIMozIntl : nsISupports
 {
   jsval getCalendarInfo([optional] in jsval locales);
   jsval getDisplayNames([optional] in jsval locales, [optional] in jsval options);
   jsval getLocaleInfo([optional] in jsval locales);
 
   jsval createPluralRules([optional] in jsval locales, [optional] in jsval options);
+  jsval createDateTimeFormat([optional] in jsval locales, [optional] in jsval options);
 };
--- a/toolkit/components/mozintl/mozIMozIntlHelper.idl
+++ b/toolkit/components/mozintl/mozIMozIntlHelper.idl
@@ -21,9 +21,32 @@ interface mozIMozIntlHelper : nsISupport
   [implicit_jscontext] void addGetLocaleInfo(in jsval intlObject);
 
   /**
    * Adds a PluralRules constructor to the given object.  This function may only
    * be called once within a realm/global object: calling it multiple times will
    * throw.
    */
   [implicit_jscontext] void addPluralRulesConstructor(in jsval intlObject);
+
+  /**
+   * Adds a MozDateTimeFormat contructor to the given object. This function may only
+   * be called once within a realm/global object: calling it multiple times will
+   * throw.
+   *
+   * The difference between regular Intl.DateTimeFormat and the method created here
+   * is that we support two more options:
+   *
+   *    timeStyle: full | long | medium | short
+   *    dateStyle: full | long | medium | short
+   *
+   * which allow user to create normalized date/time style formats.
+   * Additionally, when those options are used instead of the regular atomic
+   * options (hour, minute, month, etc.) this code will look into host
+   * Operating System regional preferences and adjust for that.
+   *
+   * That means that if user will manually select time format (hour12/24) or
+   * adjust how the date should be displayed, MozDateTimeFormat will use that.
+   *
+   * This API should be used everywhere in the UI instead of regular Intl  API.
+   */
+  [implicit_jscontext] void addDateTimeFormatConstructor(in jsval intlObject);
 };
--- a/toolkit/components/mozintl/mozIntl.js
+++ b/toolkit/components/mozintl/mozIntl.js
@@ -6,29 +6,56 @@ Components.utils.import("resource://gre/
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 const mozIntlHelper =
   Cc["@mozilla.org/mozintlhelper;1"].getService(Ci.mozIMozIntlHelper);
 const localeSvc =
   Cc["@mozilla.org/intl/localeservice;1"].getService(Ci.mozILocaleService);
+const osPrefs =
+  Cc["@mozilla.org/intl/ospreferences;1"].getService(Ci.mozIOSPreferences);
 
 /**
  * This helper function retrives currently used app locales, allowing
  * all mozIntl APIs to use the current app locales unless called with
  * explicitly listed locales.
  */
 function getLocales(locales) {
   if (!locales) {
     return localeSvc.getAppLocalesAsBCP47();
   }
   return locales;
 }
 
+function getLocale(locales) {
+  if (!locales) {
+    return localeSvc.getAppLocale();
+  }
+  if (Array.isArray(locales)) {
+    return [0];
+  }
+  return locales;
+}
+
+function getDateTimePatternStyle(option) {
+  switch (option) {
+    case "full":
+      return osPrefs.dateTimeFormatStyleFull;
+    case "long":
+      return osPrefs.dateTimeFormatStyleLong;
+    case "medium":
+      return osPrefs.dateTimeFormatStyleMedium;
+    case "short":
+      return osPrefs.dateTimeFormatStyleShort;
+    default:
+      return osPrefs.dateTimeFormatStyleNone;
+  }
+}
+
 class MozIntl {
   constructor() {
     this._cache = {};
   }
 
   getCalendarInfo(locales, ...args) {
     if (!this._cache.hasOwnProperty("getCalendarInfo")) {
       mozIntlHelper.addGetCalendarInfo(this._cache);
@@ -55,15 +82,38 @@ class MozIntl {
 
   createPluralRules(locales, ...args) {
     if (!this._cache.hasOwnProperty("PluralRules")) {
       mozIntlHelper.addPluralRulesConstructor(this._cache);
     }
 
     return new this._cache.PluralRules(getLocales(locales), ...args);
   }
+
+  createDateTimeFormat(locales, options, ...args) {
+    if (!this._cache.hasOwnProperty("DateTimeFormat")) {
+      mozIntlHelper.addDateTimeFormatConstructor(this._cache);
+    }
+
+    let resolvedLocales =
+      this._cache.DateTimeFormat.supportedLocalesOf(getLocales(locales));
+
+    if (options) {
+      if (options.dateStyle || options.timeStyle) {
+        options.pattern = osPrefs.getDateTimePattern(
+          getDateTimePatternStyle(options.dateStyle),
+          getDateTimePatternStyle(options.timeStyle),
+          resolvedLocales[0]);
+      } else {
+        // make sure that user doesn't pass a pattern explicitly
+        options.pattern = undefined;
+      }
+    }
+
+    return new this._cache.DateTimeFormat(resolvedLocales, options, ...args);
+  }
 }
 
 MozIntl.prototype.classID = Components.ID("{35ec195a-e8d0-4300-83af-c8a2cc84b4a3}");
 MozIntl.prototype.QueryInterface = XPCOMUtils.generateQI([Ci.mozIMozIntl, Ci.nsISupports]);
 
 var components = [MozIntl];
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
--- a/toolkit/components/mozintl/test/test_mozintl.js
+++ b/toolkit/components/mozintl/test/test_mozintl.js
@@ -11,17 +11,19 @@ function run_test() {
   ok(true);
 }
 
 function test_methods_presence(mozIntl) {
   equal(mozIntl.getCalendarInfo instanceof Function, true);
   equal(mozIntl.getDisplayNames instanceof Function, true);
   equal(mozIntl.getLocaleInfo instanceof Function, true);
   equal(mozIntl.createPluralRules instanceof Function, true);
+  equal(mozIntl.createDateTimeFormat instanceof Function, true);
 }
 
 function test_methods_calling(mozIntl) {
   mozIntl.getCalendarInfo("pl");
   mozIntl.getDisplayNames("ar");
   mozIntl.getLocaleInfo("de");
   mozIntl.createPluralRules("fr");
+  mozIntl.createDateTimeFormat("fr");
   ok(true);
 }
--- a/toolkit/components/mozintl/test/test_mozintlhelper.js
+++ b/toolkit/components/mozintl/test/test_mozintlhelper.js
@@ -31,20 +31,28 @@ function test_cross_global(miHelper) {
   equal(waivedX.getCalendarInfo() instanceof Object, false);
   equal(waivedX.getCalendarInfo() instanceof global.Object, true);
 }
 
 function test_methods_presence(miHelper) {
   equal(miHelper.addGetCalendarInfo instanceof Function, true);
   equal(miHelper.addGetDisplayNames instanceof Function, true);
   equal(miHelper.addGetLocaleInfo instanceof Function, true);
+  equal(miHelper.addPluralRulesConstructor instanceof Function, true);
+  equal(miHelper.addDateTimeFormatConstructor instanceof Function, true);
 
   let x = {};
 
   miHelper.addGetCalendarInfo(x);
   equal(x.getCalendarInfo instanceof Function, true);
 
   miHelper.addGetDisplayNames(x);
   equal(x.getDisplayNames instanceof Function, true);
 
   miHelper.addGetLocaleInfo(x);
   equal(x.getLocaleInfo instanceof Function, true);
+
+  miHelper.addPluralRulesConstructor(x);
+  equal(x.PluralRules instanceof Function, true);
+
+  miHelper.addDateTimeFormatConstructor(x);
+  equal(x.DateTimeFormat instanceof Function, true);
 }