WebCore on Mac ignores the user's preferred region (country) while getting the language
[WebKit.git] / Source / WebCore / platform / mac / Language.mm
1 /*
2  * Copyright (C) 2003, 2005, 2006, 2010, 2011, 2016 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "Language.h"
28
29 #import "BlockExceptions.h"
30 #import "CFBundleSPI.h"
31 #import "WebCoreNSStringExtras.h"
32 #import <mutex>
33 #import <unicode/uloc.h>
34 #import <wtf/Assertions.h>
35 #import <wtf/Lock.h>
36 #import <wtf/NeverDestroyed.h>
37 #import <wtf/RetainPtr.h>
38 #import <wtf/text/WTFString.h>
39
40 namespace WebCore {
41
42 static StaticLock preferredLanguagesMutex;
43
44 static Vector<String>& preferredLanguages()
45 {
46     static NeverDestroyed<Vector<String>> languages;
47     return languages;
48 }
49
50 }
51
52 @interface WebLanguageChangeObserver : NSObject
53 @end
54
55 @implementation WebLanguageChangeObserver
56
57 + (void)languagePreferencesDidChange:(NSNotification *)notification
58 {
59     UNUSED_PARAM(notification);
60
61     {
62         std::lock_guard<StaticLock> lock(WebCore::preferredLanguagesMutex);
63         WebCore::preferredLanguages().clear();
64     }
65
66     WebCore::languageDidChange();
67 }
68
69 @end
70
71 namespace WebCore {
72
73 static String httpStyleLanguageCode(NSString *language, NSString *country)
74 {
75     SInt32 languageCode;
76     SInt32 regionCode; 
77     SInt32 scriptCode; 
78     CFStringEncoding stringEncoding;
79     
80     bool languageDidSpecifyExplicitVariant = [language rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"-_"]].location != NSNotFound;
81
82     // FIXME: This transformation is very wrong:
83     // 1. There is no reason why CFBundle localization names would be at all related to language names as used on the Web.
84     // 2. Script Manager codes cannot represent all languages that are now supported by the platform, so the conversion is lossy.
85     // 3. This should probably match what is sent by the network layer as Accept-Language, but currently, that's implemented separately.
86     CFBundleGetLocalizationInfoForLocalization((CFStringRef)language, &languageCode, &regionCode, &scriptCode, &stringEncoding);
87     RetainPtr<CFStringRef> preferredLanguageCode = adoptCF(CFBundleCopyLocalizationForLocalizationInfo(languageCode, regionCode, scriptCode, stringEncoding));
88     if (preferredLanguageCode)
89         language = (NSString *)preferredLanguageCode.get();
90
91     // Make the string lowercase.
92     NSString *lowercaseLanguageCode = [language lowercaseString];
93     NSString *lowercaseCountryCode = [country lowercaseString];
94     
95     // If we see a "_" after a 2-letter language code:
96     // If the country is valid and the language did not specify a variant, replace the "_" and
97     // whatever comes after it with "-" followed by the country code.
98     // Otherwise, replace the "_" with a "-" and use whatever country
99     // CFBundleCopyLocalizationForLocalizationInfo() returned.
100     if ([lowercaseLanguageCode length] >= 3 && [lowercaseLanguageCode characterAtIndex:2] == '_') {
101         if (country && !languageDidSpecifyExplicitVariant)
102             return [NSString stringWithFormat:@"%@-%@", [lowercaseLanguageCode substringWithRange:NSMakeRange(0, 2)], lowercaseCountryCode];
103         
104         // Fall back to older behavior, which used the original language-based code but just changed
105         // the "_" to a "-".
106         RetainPtr<NSMutableString> mutableLanguageCode = adoptNS([lowercaseLanguageCode mutableCopy]);
107         [mutableLanguageCode.get() replaceCharactersInRange:NSMakeRange(2, 1) withString:@"-"];
108         return mutableLanguageCode.get();
109     }
110
111     return lowercaseLanguageCode;
112 }
113
114 static bool isValidICUCountryCode(NSString* countryCode)
115 {
116     const char* const* countries = uloc_getISOCountries();
117     const char* countryUTF8 = [countryCode UTF8String];
118     for (unsigned i = 0; countries[i]; ++i) {
119         const char* possibleCountry = countries[i];
120         if (!strcmp(countryUTF8, possibleCountry))
121             return true;
122     }
123     return false;
124 }
125
126 Vector<String> platformUserPreferredLanguages()
127 {
128 #if PLATFORM(MAC)
129     static dispatch_once_t onceToken;
130     dispatch_once(&onceToken, ^{
131         [[NSDistributedNotificationCenter defaultCenter] addObserver:[WebLanguageChangeObserver self] selector:@selector(languagePreferencesDidChange:) name:@"AppleLanguagePreferencesChangedNotification" object:nil];
132     });
133 #endif
134
135     BEGIN_BLOCK_OBJC_EXCEPTIONS;
136
137     std::lock_guard<StaticLock> lock(preferredLanguagesMutex);
138     Vector<String>& userPreferredLanguages = preferredLanguages();
139
140     if (userPreferredLanguages.isEmpty()) {
141         RetainPtr<CFLocaleRef> locale = adoptCF(CFLocaleCopyCurrent());
142         NSString *countryCode = (NSString *)CFLocaleGetValue(locale.get(), kCFLocaleCountryCode);
143         
144         if (!isValidICUCountryCode(countryCode))
145             countryCode = nil;
146         
147         RetainPtr<CFArrayRef> languages = adoptCF(CFLocaleCopyPreferredLanguages());
148         CFIndex languageCount = CFArrayGetCount(languages.get());
149         if (!languageCount)
150             userPreferredLanguages.append("en");
151         else {
152             for (CFIndex i = 0; i < languageCount; i++)
153                 userPreferredLanguages.append(httpStyleLanguageCode((NSString *)CFArrayGetValueAtIndex(languages.get(), i), countryCode));
154         }
155     }
156
157     Vector<String> userPreferredLanguagesCopy;
158     userPreferredLanguagesCopy.reserveInitialCapacity(userPreferredLanguages.size());
159
160     for (auto& language : userPreferredLanguages)
161         userPreferredLanguagesCopy.uncheckedAppend(language.isolatedCopy());
162
163     return userPreferredLanguagesCopy;
164
165     END_BLOCK_OBJC_EXCEPTIONS;
166
167     return Vector<String>();
168 }
169
170 }