Bug 1339650 - Introduce mozIntl.DateTimeFormat. r=jfkthame
MozReview-Commit-ID: 1jnit4IlDN6
--- 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);
}