// Copyright (c) 2012 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/strings/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 language 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[] = {
#define HANDLE_LANGUAGE(l_, o_) { L ## #l_, o_ },
DO_LANGUAGES
#undef HANDLE_LANGUAGE
};
// A sorted array of language identifiers that are aliases to other languages
// for which translations are available.
const LangToOffset kLanguageToOffsetExceptions[] = {
// Alias some English variants to British English (all others wildcard to US).
{ L"en-au", IDS_L10N_OFFSET_EN_GB },
{ L"en-ca", IDS_L10N_OFFSET_EN_GB },
{ L"en-nz", IDS_L10N_OFFSET_EN_GB },
{ L"en-za", IDS_L10N_OFFSET_EN_GB },
// Alias es-es to es (all others wildcard to es-419).
{ L"es-es", IDS_L10N_OFFSET_ES },
// 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 Hong Kong and Macau to Taiwan.
{ L"zh-hk", IDS_L10N_OFFSET_ZH_TW },
{ L"zh-mo", IDS_L10N_OFFSET_ZH_TW },
// Although the wildcard entry for zh would result in this, alias zh-sg so
// that it will win if it precedes another valid tag in a list of candidates.
{ L"zh-sg", IDS_L10N_OFFSET_ZH_CN }
};
// 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 },
// Use the Latin American region for anything Spanish.
{ L"es", IDS_L10N_OFFSET_ES_419 },
// Use the Brazil region for anything Portugese.
{ L"pt", IDS_L10N_OFFSET_PT_BR },
// Use the P.R.C. region for anything Chinese.
{ L"zh", IDS_L10N_OFFSET_ZH_CN }
};
#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;
}
// 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 primary_language = language.substr(0, language.find(L'-'));
// Now check for wildcards.
return
TryFindOffset(
&kLanguageToOffsetWildcards[0],
&kLanguageToOffsetWildcards[arraysize(kLanguageToOffsetWildcards)],
primary_language, 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 {
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