Bug 1339650 - Introduce mozIntl.DateTimeFormat. r=jfkthame
authorZibi Braniecki <gandalf@mozilla.com>
Thu, 02 Mar 2017 16:39:17 -0800
changeset 350637 55647bd1ea9ddbe41cc9829b1b562a9cd0fd81a3
parent 350636 d3ba13b92330ef1e371a589f1649c57a995c0ec0
child 350638 79eeca3860563e089c3163fb5137b1be90fb31a6
push id88685
push usercbook@mozilla.com
push dateFri, 31 Mar 2017 12:48:11 +0000
treeherdermozilla-inbound@aece477a513e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjfkthame
bugs1339650
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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);
 }