Chromium Code Reviews| Index: chrome/installer/util/language_selector.cc |
| =================================================================== |
| --- chrome/installer/util/language_selector.cc (revision 0) |
| +++ chrome/installer/util/language_selector.cc (revision 0) |
| @@ -0,0 +1,339 @@ |
| +// Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| +// |
| +// This file defines a helper class for selecting a supported language from a |
| +// set of candidates. |
| + |
| +#include "chrome/installer/util/language_selector.h" |
| + |
| +#include <algorithm> |
| +#include <functional> |
| + |
| +#include "base/logging.h" |
| +#include "base/string_util.h" |
| +#include "base/win/i18n.h" |
| +#include "chrome/installer/util/google_update_settings.h" |
| + |
| +#include "installer_util_strings.h" |
| + |
| +namespace { |
| + |
| +struct LangToOffset { |
| + const wchar_t* language; |
| + int offset; |
| +}; |
| + |
| +// The langauge we fall back upon when all else fails. |
| +const wchar_t kFallbackLanguage[] = L"en-us"; |
| +const int kFallbackLanguageOffset = IDS_L10N_OFFSET_EN_US; |
| + |
| +// http://tools.ietf.org/html/rfc5646 Section 2.3.3 |
| +const std::wstring::size_type kScriptSubtagLength = 4; |
| + |
| +// A sorted array of language identifiers (and their offsets) for which |
| +// translations are available. The contents of the array are generated by |
| +// create_string_rc.py. |
| +const LangToOffset kLanguageOffsetPairs[] = { |
| +#if defined(GOOGLE_CHROME_BUILD) |
| +#define HANDLE_LANGUAGE(l_, o_) { L#l_, o_ }, |
| + DO_LANGUAGES |
| +#undef HANDLE_LANGUAGE |
| +#else // defined(GOOGLE_CHROME_BUILD) |
| + { &kFallbackLanguage[0], kFallbackLanguageOffset } |
| +#endif // !defined(GOOGLE_CHROME_BUILD) |
| +}; |
| + |
| +// A sorted array of language identifiers that are aliases to other languages |
| +// for which translations are available. |
| +const LangToOffset kLanguageToOffsetExceptions[] = { |
| +#if defined(GOOGLE_CHROME_BUILD) |
| + // Google web properties use iw for he. Handle both just to be safe. |
| + { L"he", IDS_L10N_OFFSET_IW }, |
| + // Google web properties use no for nb. Handle both just to be safe. |
| + { L"nb", IDS_L10N_OFFSET_NO }, |
| + // Some Google web properties use tl for fil. Handle both just to be safe. |
| + // They're not completely identical, but alias it here. |
| + { L"tl", IDS_L10N_OFFSET_FIL }, |
| + // Pre-Vista aliases for Chinese w/ script subtag. |
| + { L"zh-chs", IDS_L10N_OFFSET_ZH_CN }, |
| + { L"zh-cht", IDS_L10N_OFFSET_ZH_TW }, |
| + // Vista+ aliases for Chinese w/ script subtag. |
| + { L"zh-hans", IDS_L10N_OFFSET_ZH_CN }, |
| + { L"zh-hant", IDS_L10N_OFFSET_ZH_TW }, |
| + // Alias Macau and Hong Kong to Taiwan. |
| + { L"zh-hk", IDS_L10N_OFFSET_ZH_TW }, |
| + { L"zh-mk", IDS_L10N_OFFSET_ZH_TW }, |
| + // Windows uses "mo" for Macau. |
| + { L"zh-mo", IDS_L10N_OFFSET_ZH_TW } |
|
jungshik at Google
2010/11/11 20:30:45
Does Windows have zh-SG (Singpore)? If so, zh-sg h
grt (UTC plus 2)
2010/11/12 04:22:02
Yes, Windows does. This is handled by the wildcar
|
| +#else // defined(GOOGLE_CHROME_BUILD) |
| + // An empty array is no good, so repeat the fallback. |
| + { &kFallbackLanguage[0], kFallbackLanguageOffset } |
| +#endif // !defined(GOOGLE_CHROME_BUILD) |
| +}; |
| + |
| +// A sorted array of neutral language identifiers that are wildcard aliases to |
| +// other languages for which translations are available. |
| +const LangToOffset kLanguageToOffsetWildcards[] = { |
| + // Use the U.S. region for anything English. |
| + { L"en", IDS_L10N_OFFSET_EN_US }, |
| +#if defined(GOOGLE_CHROME_BUILD) |
| + // Use the Latin American region for anything Spanish. |
| + { L"es", IDS_L10N_OFFSET_ES_419 }, |
| + // Use the Portugal region for anything Portugese. |
|
jungshik at Google
2010/11/11 20:30:45
I guess it's better to map this to pt_BR (Brazil i
grt (UTC plus 2)
2010/11/12 04:22:02
The previous behavior was to only use pt-br when i
|
| + { L"pt", IDS_L10N_OFFSET_PT_PT }, |
| + // Use the P.R.C. region for anything Chinese. |
| + { L"zh", IDS_L10N_OFFSET_ZH_CN } |
| +#endif // defined(GOOGLE_CHROME_BUILD) |
| +}; |
| + |
| +#if !defined(NDEBUG) |
| +// Returns true if the items in the given range are sorted. If |
| +// |byNameAndOffset| is true, the items must be sorted by both name and offset. |
| +bool IsArraySorted(const LangToOffset* first, const LangToOffset* last, |
| + bool byNameAndOffset) { |
| + if (last - first > 1) { |
| + for (--last; first != last; ++first) { |
| + if (!(std::wstring(first->language) < (first + 1)->language) || |
| + byNameAndOffset && !(first->offset < (first + 1)->offset)) { |
| + return false; |
| + } |
| + } |
| + } |
| + return true; |
| +} |
| + |
| +// Validates that the static read-only mappings are properly sorted. |
| +void ValidateMappings() { |
| + // Ensure that kLanguageOffsetPairs is sorted. |
| + DCHECK(IsArraySorted(&kLanguageOffsetPairs[0], |
| + &kLanguageOffsetPairs[arraysize(kLanguageOffsetPairs)], |
| + true)) << "kOffsetToLanguageId is not sorted"; |
| + |
| + // Ensure that kLanguageToOffsetExceptions is sorted. |
| + DCHECK(IsArraySorted( |
| + &kLanguageToOffsetExceptions[0], |
| + &kLanguageToOffsetExceptions[arraysize(kLanguageToOffsetExceptions)], |
| + false)) << "kLanguageToOffsetExceptions is not sorted"; |
| + |
| + // Ensure that kLanguageToOffsetWildcards is sorted. |
| + DCHECK(IsArraySorted( |
| + &kLanguageToOffsetWildcards[0], |
| + &kLanguageToOffsetWildcards[arraysize(kLanguageToOffsetWildcards)], |
| + false)) << "kLanguageToOffsetWildcards is not sorted"; |
| +} |
| +#endif // !defined(NDEBUG) |
| + |
| +// A less-than overload to do slightly more efficient searches in the |
| +// sorted arrays. |
| +bool operator<(const LangToOffset& left, const std::wstring& right) { |
| + return left.language < right; |
| +} |
| + |
| +// A less-than overload to do slightly more efficient searches in the |
| +// sorted arrays. |
| +bool operator<(const std::wstring& left, const LangToOffset& right) { |
| + return left < right.language; |
| +} |
| + |
| +// A not-so-efficient less-than overload for the same uses as above. |
| +bool operator<(const LangToOffset& left, const LangToOffset& right) { |
| + return std::wstring(left.language) < right.language; |
| +} |
| + |
| +// A compare function for searching in a sorted array by offset. |
| +bool IsOffsetLessThan(const LangToOffset& left, const LangToOffset& right) { |
| + return left.offset < right.offset; |
| +} |
| + |
| +// Binary search in one of the sorted arrays to find the offset corresponding to |
| +// a given language |name|. |
| +bool TryFindOffset(const LangToOffset* first, const LangToOffset* last, |
| + const std::wstring& name, int* offset) { |
| + const LangToOffset* search_result = std::lower_bound(first, last, name); |
| + if (last != search_result && search_result->language == name) { |
| + *offset = search_result->offset; |
| + return true; |
| + } |
| + return false; |
| +} |
| + |
| +// Splits a language tag into |language_part|[-|script_part|][-|region_part|]. |
| +void CrackLanguageTag(const std::wstring& language, std::wstring* language_part, |
| + std::wstring* script_part, std::wstring* region_part) { |
|
jungshik at Google
2010/11/11 20:30:45
Have you considered using ICU's Locale class? That
grt (UTC plus 2)
2010/11/12 04:22:02
Thanks for the pointer. As it happens, I'd have t
|
| + std::wstring::size_type first_dash_pos = language.find(L'-'); |
| + std::wstring::size_type second_dash_pos = |
| + language.find(L'-', first_dash_pos + 1); |
| + DCHECK(std::wstring::npos == second_dash_pos || |
| + std::wstring::npos == language.find(L'-', second_dash_pos + 1)) |
| + << "Language tags with more than three subtags are not supported."; |
| + |
| + // Everything before the first dash is the primary language subtag. |
| + if (0 != first_dash_pos && std::wstring::npos != first_dash_pos) { |
| + language_part->assign(language, 0, first_dash_pos); |
| + } |
| + |
| + if (second_dash_pos == std::wstring::npos) { |
| + // There are only two subtags. If the second is four characters, consider |
| + // it to be the script subtag; otherwise, consider it the region subtag. |
| + if (language.size() - kScriptSubtagLength - 1 == first_dash_pos) { |
| + script_part->assign(language, first_dash_pos + 1, kScriptSubtagLength); |
| + region_part->clear(); |
| + } else { |
| + script_part->clear(); |
| + region_part->assign(language, first_dash_pos + 1, std::wstring::npos); |
| + } |
| + } else { |
| + // There are three or more subtags. Consider the second to be a script |
| + // subtag regardless of its length (pre-Vista had things like zh-chs). |
| + VLOG_IF(1, second_dash_pos - kScriptSubtagLength - 1 != first_dash_pos) |
| + << "Returning a script subtag that is not four chars long."; |
| + script_part->assign(language, first_dash_pos + 1, |
| + second_dash_pos - first_dash_pos - 1); |
| + region_part->assign(language, second_dash_pos + 1, std::wstring::npos); |
| + } |
| +} |
| + |
| +// A predicate function for LanguageSelector::SelectIf that searches for the |
| +// offset of a translated language. The search first tries to find an exact |
| +// match. Failing that, an exact match with an alias is attempted. |
| +bool GetLanguageOffset(const std::wstring& language, int* offset) { |
| + // Note: always perform the exact match first so that an alias is never |
| + // selected in place of a future translation. |
| + return |
| + TryFindOffset( |
| + &kLanguageOffsetPairs[0], |
| + &kLanguageOffsetPairs[arraysize(kLanguageOffsetPairs)], |
| + language, offset) || |
| + TryFindOffset( |
| + &kLanguageToOffsetExceptions[0], |
| + &kLanguageToOffsetExceptions[arraysize(kLanguageToOffsetExceptions)], |
| + language, offset); |
| +} |
| + |
| +// A predicate function for LanguageSelector::SelectIf that searches for a |
| +// wildcard match with |language|'s primary language subtag. |
| +bool MatchLanguageOffset(const std::wstring& language, int* offset) { |
| + std::wstring language_part; |
| + std::wstring script_part; |
| + std::wstring region_part; |
| + |
| + // split lang[-script][-reg] |
| + CrackLanguageTag(language, &language_part, &script_part, ®ion_part); |
| + |
| + // Now check for wildcards. |
| + return |
| + TryFindOffset( |
| + &kLanguageToOffsetWildcards[0], |
| + &kLanguageToOffsetWildcards[arraysize(kLanguageToOffsetWildcards)], |
| + language_part, offset); |
| +} |
| + |
| +// Adds to |candidates| the eligible languages on the system. Any language |
| +// setting specified by Omaha takes precedence over the operating system's |
| +// configured languages. |
| +void GetCandidatesFromSystem(std::vector<std::wstring>* candidates) { |
| + DCHECK(candidates); |
| + std::wstring language; |
| + |
| + // Omaha gets first pick. |
| + GoogleUpdateSettings::GetLanguage(&language); |
| + if (!language.empty()) { |
| + candidates->push_back(language); |
| + } |
| + |
| + // Now try the Windows UI languages. Use the thread preferred since that will |
| + // kindly return us a list of all kinds of fallbacks. |
| + base::win::i18n::GetThreadPreferredUILanguageList(candidates); |
| +} |
| + |
| +} // namespace |
| + |
| +namespace installer_util { |
| + |
| +LanguageSelector::LanguageSelector() |
| + : offset_(arraysize(kLanguageOffsetPairs)) { |
| +#if !defined(NDEBUG) |
| + ValidateMappings(); |
| +#endif // !defined(NDEBUG) |
| + std::vector<std::wstring> candidates; |
| + |
| + GetCandidatesFromSystem(&candidates); |
| + DoSelect(candidates); |
| +} |
| + |
| +LanguageSelector::LanguageSelector(const std::vector<std::wstring>& candidates) |
| + : offset_(arraysize(kLanguageOffsetPairs)) { |
| +#if !defined(NDEBUG) |
| + ValidateMappings(); |
| +#endif // !defined(NDEBUG) |
| + DoSelect(candidates); |
| +} |
| + |
| +LanguageSelector::~LanguageSelector() { |
| +} |
| + |
| +// static |
| +std::wstring LanguageSelector::GetLanguageName(int offset) { |
| + DCHECK_GE(offset, 0); |
| + DCHECK_LT(static_cast<size_t>(offset), arraysize(kLanguageOffsetPairs)); |
| + |
| + LangToOffset value = { NULL, offset }; |
| + const LangToOffset* search_result = |
| + std::lower_bound(&kLanguageOffsetPairs[0], |
| + &kLanguageOffsetPairs[arraysize(kLanguageOffsetPairs)], |
| + value, IsOffsetLessThan); |
| + if (&kLanguageOffsetPairs[arraysize(kLanguageOffsetPairs)] != search_result && |
| + search_result->offset == offset) { |
| + return search_result->language; |
| + } |
| + NOTREACHED() << "Unknown language offset."; |
| + return std::wstring(&kFallbackLanguage[0], arraysize(kFallbackLanguage) - 1); |
| +} |
| + |
| +// Runs through the set of candidates, sending their downcased representation |
| +// through |select_predicate|. Returns true if the predicate selects a |
| +// candidate, in which case |matched_name| is assigned the value of the |
| +// candidate and |matched_offset| is assigned the language offset of the |
| +// selected translation. |
| +// static |
| +bool LanguageSelector::SelectIf(const std::vector<std::wstring>& candidates, |
| + SelectPred_Fn select_predicate, |
| + std::wstring* matched_name, |
| + int* matched_offset) { |
| + std::wstring candidate; |
| + for (std::vector<std::wstring>::const_iterator scan = candidates.begin(), |
| + end = candidates.end(); scan != end; ++scan) { |
| + candidate.assign(*scan); |
| + StringToLowerASCII(&candidate); |
| + if (select_predicate(candidate, matched_offset)) { |
| + matched_name->assign(*scan); |
| + return true; |
| + } |
| + } |
| + |
| + return false; |
| +} |
| + |
| +// Select the best-fit translation from the ordered list |candidates|. |
| +// At the conclusion, this instance's |matched_candidate_| and |offset_| members |
| +// are set to the name of the selected candidate and the offset of the matched |
| +// translation. If no translation is selected, the fallback's name and offset |
| +// are selected. |
| +void LanguageSelector::DoSelect(const std::vector<std::wstring>& candidates) { |
| + // Make a pass through the candidates looking for an exact or alias match. |
| + // Failing that, make another pass looking for a wildcard match. |
| + if (!SelectIf(candidates, &GetLanguageOffset, &matched_candidate_, |
| + &offset_) && |
| + !SelectIf(candidates, &MatchLanguageOffset, &matched_candidate_, |
| + &offset_)) { |
| + VLOG(1) << "No suitable language found for any candidates."; |
| + |
| + // Our fallback is "en-us" |
| + matched_candidate_.assign(&kFallbackLanguage[0], |
| + arraysize(kFallbackLanguage) - 1); |
| + offset_ = kFallbackLanguageOffset; |
| + } |
| +} |
| + |
| +} // namespace installer_util |