1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "OSPreferences.h"
8 #include "mozilla/intl/Locale.h"
9 #include "mozilla/intl/LocaleService.h"
10 #include "mozilla/WindowsVersion.h"
11 #include "nsReadableUtils.h"
12
13 #include <windows.h>
14
15 #ifndef __MINGW32__ // WinRT headers not yet supported by MinGW
16 # include <roapi.h>
17 # include <wrl.h>
18 # include <Windows.System.UserProfile.h>
19
20 using namespace Microsoft::WRL;
21 using namespace Microsoft::WRL::Wrappers;
22 using namespace ABI::Windows::Foundation::Collections;
23 using namespace ABI::Windows::System::UserProfile;
24 #endif
25
26 using namespace mozilla::intl;
27
OSPreferences()28 OSPreferences::OSPreferences() {}
29
ReadSystemLocales(nsTArray<nsCString> & aLocaleList)30 bool OSPreferences::ReadSystemLocales(nsTArray<nsCString>& aLocaleList) {
31 MOZ_ASSERT(aLocaleList.IsEmpty());
32
33 #ifndef __MINGW32__
34 if (IsWin8OrLater()) {
35 // Try to get language list from GlobalizationPreferences; if this fails,
36 // we'll fall back to GetUserPreferredUILanguages.
37 // Per MSDN, these APIs are not available prior to Win8.
38
39 // RoInitialize may fail with "cannot change thread mode after it is set",
40 // if the runtime was already initialized on this thread.
41 // This appears to be harmless, and we can proceed to attempt the following
42 // runtime calls.
43 HRESULT inited = RoInitialize(RO_INIT_MULTITHREADED);
44 if (SUCCEEDED(inited) || inited == RPC_E_CHANGED_MODE) {
45 ComPtr<IGlobalizationPreferencesStatics> globalizationPrefs;
46 ComPtr<IVectorView<HSTRING>> languages;
47 uint32_t count;
48 if (SUCCEEDED(RoGetActivationFactory(
49 HStringReference(
50 RuntimeClass_Windows_System_UserProfile_GlobalizationPreferences)
51 .Get(),
52 IID_PPV_ARGS(&globalizationPrefs))) &&
53 SUCCEEDED(globalizationPrefs->get_Languages(&languages)) &&
54 SUCCEEDED(languages->get_Size(&count))) {
55 for (uint32_t i = 0; i < count; ++i) {
56 HString lang;
57 if (SUCCEEDED(languages->GetAt(i, lang.GetAddressOf()))) {
58 unsigned int length;
59 const wchar_t* text = lang.GetRawBuffer(&length);
60 NS_LossyConvertUTF16toASCII loc(text, length);
61 if (CanonicalizeLanguageTag(loc)) {
62 if (!loc.Contains('-')) {
63 // DirectWrite font-name code doesn't like to be given a bare
64 // language code with no region subtag, but the
65 // GlobalizationPreferences API may give us one (e.g. "ja").
66 // So if there's no hyphen in the string at this point, we use
67 // AddLikelySubtags to get a suitable region code to
68 // go with it.
69 Locale locale;
70 auto result = LocaleParser::TryParse(loc, locale);
71 if (result.isOk() && locale.AddLikelySubtags().isOk() &&
72 locale.Region().Present()) {
73 loc.Append('-');
74 loc.Append(locale.Region().Span());
75 }
76 }
77 aLocaleList.AppendElement(loc);
78 }
79 }
80 }
81 }
82 }
83 // Only close the runtime if we successfully initialized it above,
84 // otherwise we assume it was already in use and should be left as is.
85 if (SUCCEEDED(inited)) {
86 RoUninitialize();
87 }
88 }
89 #endif
90
91 // Per MSDN, GetUserPreferredUILanguages is available from Vista onwards,
92 // so we can use it unconditionally (although it may not work well!)
93 if (aLocaleList.IsEmpty()) {
94 // Note that according to the questioner at
95 // https://stackoverflow.com/questions/52849233/getuserpreferreduilanguages-never-returns-more-than-two-languages,
96 // this may not always return the full list of languages we'd expect.
97 // We should always get at least the first-preference lang, though.
98 ULONG numLanguages = 0;
99 DWORD cchLanguagesBuffer = 0;
100 if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numLanguages, nullptr,
101 &cchLanguagesBuffer)) {
102 return false;
103 }
104
105 AutoTArray<WCHAR, 64> locBuffer;
106 locBuffer.SetCapacity(cchLanguagesBuffer);
107 if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numLanguages,
108 locBuffer.Elements(),
109 &cchLanguagesBuffer)) {
110 return false;
111 }
112
113 const WCHAR* start = locBuffer.Elements();
114 const WCHAR* bufEnd = start + cchLanguagesBuffer;
115 while (bufEnd - start > 1 && *start) {
116 const WCHAR* end = start + 1;
117 while (bufEnd - end > 1 && *end) {
118 end++;
119 }
120 NS_LossyConvertUTF16toASCII loc(start, end - start);
121 if (CanonicalizeLanguageTag(loc)) {
122 aLocaleList.AppendElement(loc);
123 }
124 start = end + 1;
125 }
126 }
127
128 return !aLocaleList.IsEmpty();
129 }
130
ReadRegionalPrefsLocales(nsTArray<nsCString> & aLocaleList)131 bool OSPreferences::ReadRegionalPrefsLocales(nsTArray<nsCString>& aLocaleList) {
132 MOZ_ASSERT(aLocaleList.IsEmpty());
133
134 WCHAR locale[LOCALE_NAME_MAX_LENGTH];
135 if (NS_WARN_IF(!LCIDToLocaleName(LOCALE_USER_DEFAULT, locale,
136 LOCALE_NAME_MAX_LENGTH, 0))) {
137 return false;
138 }
139
140 NS_LossyConvertUTF16toASCII loc(locale);
141
142 if (CanonicalizeLanguageTag(loc)) {
143 aLocaleList.AppendElement(loc);
144 return true;
145 }
146 return false;
147 }
148
ToDateLCType(OSPreferences::DateTimeFormatStyle aFormatStyle)149 static LCTYPE ToDateLCType(OSPreferences::DateTimeFormatStyle aFormatStyle) {
150 switch (aFormatStyle) {
151 case OSPreferences::DateTimeFormatStyle::None:
152 return LOCALE_SLONGDATE;
153 case OSPreferences::DateTimeFormatStyle::Short:
154 return LOCALE_SSHORTDATE;
155 case OSPreferences::DateTimeFormatStyle::Medium:
156 return LOCALE_SSHORTDATE;
157 case OSPreferences::DateTimeFormatStyle::Long:
158 return LOCALE_SLONGDATE;
159 case OSPreferences::DateTimeFormatStyle::Full:
160 return LOCALE_SLONGDATE;
161 case OSPreferences::DateTimeFormatStyle::Invalid:
162 default:
163 MOZ_ASSERT_UNREACHABLE("invalid date format");
164 return LOCALE_SLONGDATE;
165 }
166 }
167
ToTimeLCType(OSPreferences::DateTimeFormatStyle aFormatStyle)168 static LCTYPE ToTimeLCType(OSPreferences::DateTimeFormatStyle aFormatStyle) {
169 switch (aFormatStyle) {
170 case OSPreferences::DateTimeFormatStyle::None:
171 return LOCALE_STIMEFORMAT;
172 case OSPreferences::DateTimeFormatStyle::Short:
173 return LOCALE_SSHORTTIME;
174 case OSPreferences::DateTimeFormatStyle::Medium:
175 return LOCALE_SSHORTTIME;
176 case OSPreferences::DateTimeFormatStyle::Long:
177 return LOCALE_STIMEFORMAT;
178 case OSPreferences::DateTimeFormatStyle::Full:
179 return LOCALE_STIMEFORMAT;
180 case OSPreferences::DateTimeFormatStyle::Invalid:
181 default:
182 MOZ_ASSERT_UNREACHABLE("invalid time format");
183 return LOCALE_STIMEFORMAT;
184 }
185 }
186
187 /**
188 * Windows API includes regional preferences from the user only
189 * if we pass empty locale string or if the locale string matches
190 * the current locale.
191 *
192 * Since Windows API only allows us to retrieve two options - short/long
193 * we map it to our four options as:
194 *
195 * short -> short
196 * medium -> short
197 * long -> long
198 * full -> long
199 *
200 * In order to produce a single date/time format, we use CLDR pattern
201 * for combined date/time string, since Windows API does not provide an
202 * option for this.
203 */
ReadDateTimePattern(DateTimeFormatStyle aDateStyle,DateTimeFormatStyle aTimeStyle,const nsACString & aLocale,nsACString & aRetVal)204 bool OSPreferences::ReadDateTimePattern(DateTimeFormatStyle aDateStyle,
205 DateTimeFormatStyle aTimeStyle,
206 const nsACString& aLocale,
207 nsACString& aRetVal) {
208 nsAutoString localeName;
209 CopyASCIItoUTF16(aLocale, localeName);
210
211 bool isDate = aDateStyle != DateTimeFormatStyle::None &&
212 aDateStyle != DateTimeFormatStyle::Invalid;
213 bool isTime = aTimeStyle != DateTimeFormatStyle::None &&
214 aTimeStyle != DateTimeFormatStyle::Invalid;
215
216 // If both date and time are wanted, we'll initially read them into a
217 // local string, and then insert them into the overall date+time pattern;
218 nsAutoString str;
219 if (isDate && isTime) {
220 if (!GetDateTimeConnectorPattern(aLocale, aRetVal)) {
221 NS_WARNING("failed to get date/time connector");
222 aRetVal.AssignLiteral("{1} {0}");
223 }
224 } else if (!isDate && !isTime) {
225 aRetVal.Truncate(0);
226 return true;
227 }
228
229 if (isDate) {
230 LCTYPE lcType = ToDateLCType(aDateStyle);
231 size_t len = GetLocaleInfoEx(
232 reinterpret_cast<const wchar_t*>(localeName.BeginReading()), lcType,
233 nullptr, 0);
234 if (len == 0) {
235 return false;
236 }
237
238 // We're doing it to ensure the terminator will fit when Windows writes the
239 // data to its output buffer. See bug 1358159 for details.
240 str.SetLength(len);
241 GetLocaleInfoEx(reinterpret_cast<const wchar_t*>(localeName.BeginReading()),
242 lcType, (WCHAR*)str.BeginWriting(), len);
243 str.SetLength(len - 1); // -1 because len counts the null terminator
244
245 // Windows uses "ddd" and "dddd" for abbreviated and full day names
246 // respectively,
247 // https://msdn.microsoft.com/en-us/library/windows/desktop/dd317787(v=vs.85).aspx
248 // but in a CLDR/ICU-style pattern these should be "EEE" and "EEEE".
249 // http://userguide.icu-project.org/formatparse/datetime
250 // So we fix that up here.
251 nsAString::const_iterator start, pos, end;
252 start = str.BeginReading(pos);
253 str.EndReading(end);
254 if (FindInReadable(u"dddd"_ns, pos, end)) {
255 str.ReplaceLiteral(pos - start, 4, u"EEEE");
256 } else {
257 pos = start;
258 if (FindInReadable(u"ddd"_ns, pos, end)) {
259 str.ReplaceLiteral(pos - start, 3, u"EEE");
260 }
261 }
262
263 // Also, Windows uses lowercase "g" or "gg" for era, but ICU wants uppercase
264 // "G" (it would interpret "g" as "modified Julian day"!). So fix that.
265 int32_t index = str.FindChar('g');
266 if (index >= 0) {
267 str.Replace(index, 1, 'G');
268 // If it was a double "gg", just drop the second one.
269 index++;
270 if (str.CharAt(index) == 'g') {
271 str.Cut(index, 1);
272 }
273 }
274
275 // If time was also requested, we need to substitute the date pattern from
276 // Windows into the date+time format that we have in aRetVal.
277 if (isTime) {
278 nsACString::const_iterator start, pos, end;
279 start = aRetVal.BeginReading(pos);
280 aRetVal.EndReading(end);
281 if (FindInReadable("{1}"_ns, pos, end)) {
282 aRetVal.Replace(pos - start, 3, NS_ConvertUTF16toUTF8(str));
283 }
284 } else {
285 aRetVal = NS_ConvertUTF16toUTF8(str);
286 }
287 }
288
289 if (isTime) {
290 LCTYPE lcType = ToTimeLCType(aTimeStyle);
291 size_t len = GetLocaleInfoEx(
292 reinterpret_cast<const wchar_t*>(localeName.BeginReading()), lcType,
293 nullptr, 0);
294 if (len == 0) {
295 return false;
296 }
297
298 // We're doing it to ensure the terminator will fit when Windows writes the
299 // data to its output buffer. See bug 1358159 for details.
300 str.SetLength(len);
301 GetLocaleInfoEx(reinterpret_cast<const wchar_t*>(localeName.BeginReading()),
302 lcType, (WCHAR*)str.BeginWriting(), len);
303 str.SetLength(len - 1);
304
305 // Windows uses "t" or "tt" for a "time marker" (am/pm indicator),
306 // https://msdn.microsoft.com/en-us/library/windows/desktop/dd318148(v=vs.85).aspx
307 // but in a CLDR/ICU-style pattern that should be "a".
308 // http://userguide.icu-project.org/formatparse/datetime
309 // So we fix that up here.
310 int32_t index = str.FindChar('t');
311 if (index >= 0) {
312 str.Replace(index, 1, 'a');
313 index++;
314 if (str.CharAt(index) == 't') {
315 str.Cut(index, 1);
316 }
317 }
318
319 if (isDate) {
320 nsACString::const_iterator start, pos, end;
321 start = aRetVal.BeginReading(pos);
322 aRetVal.EndReading(end);
323 if (FindInReadable("{0}"_ns, pos, end)) {
324 aRetVal.Replace(pos - start, 3, NS_ConvertUTF16toUTF8(str));
325 }
326 } else {
327 aRetVal = NS_ConvertUTF16toUTF8(str);
328 }
329 }
330
331 return true;
332 }
333
RemoveObservers()334 void OSPreferences::RemoveObservers() {}
335