// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #include "pch.h" #include "LocalizationService.h" #include "LocalizationSettings.h" #include "AppResourceProvider.h" using namespace CalculatorApp::Common; using namespace CalculatorApp::Common::LocalizationServiceProperties; using namespace Concurrency; using namespace Platform; using namespace Platform::Collections; using namespace std; using namespace Windows::ApplicationModel::Resources; using namespace Windows::ApplicationModel::Resources::Core; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; using namespace Windows::Globalization; using namespace Windows::Globalization::DateTimeFormatting; using namespace Windows::Globalization::Fonts; using namespace Windows::Globalization::NumberFormatting; using namespace Windows::System::UserProfile; using namespace Windows::UI::Text; using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Controls::Primitives; using namespace Windows::UI::Xaml::Documents; using namespace Windows::UI::Xaml::Media; DEPENDENCY_PROPERTY_INITIALIZATION(LocalizationService, FontType); DEPENDENCY_PROPERTY_INITIALIZATION(LocalizationService, FontSize); static reader_writer_lock s_locServiceInstanceLock; LocalizationService^ LocalizationService::s_singletonInstance = nullptr; // Resources for the engine use numbers as keys. It's inconvenient, but also difficult to // change given that the engine heavily relies on perfect ordering of certain elements. // The key for open parenthesis, '(', is "48". static constexpr auto s_openParenResourceKey = L"48"; LocalizationService^ LocalizationService::GetInstance() { if (s_singletonInstance == nullptr) { // Writer lock for the static maps reader_writer_lock::scoped_lock lock(s_locServiceInstanceLock); if (s_singletonInstance == nullptr) { s_singletonInstance = ref new LocalizationService(); } } return s_singletonInstance; } LocalizationService::LocalizationService() { m_language = ApplicationLanguages::Languages->GetAt(0); m_flowDirection = ResourceContext::GetForCurrentView()->QualifierValues->Lookup(L"LayoutDirection") != L"LTR" ? FlowDirection::RightToLeft : FlowDirection::LeftToRight; auto resourceLoader = AppResourceProvider::GetInstance(); m_fontFamilyOverride = resourceLoader.GetResourceString(L"LocalizedFontFamilyOverride"); String^ reserved = L"RESERVED_FOR_FONTLOC"; m_overrideFontApiValues = ((m_fontFamilyOverride != nullptr) && (m_fontFamilyOverride != reserved)); if (m_overrideFontApiValues) { String^ localizedUICaptionFontSizeFactorOverride = resourceLoader.GetResourceString(L"LocalizedUICaptionFontSizeFactorOverride"); String^ localizedUITextFontSizeFactorOverride = resourceLoader.GetResourceString(L"LocalizedUITextFontSizeFactorOverride"); String^ localizedFontWeightOverride = resourceLoader.GetResourceString(L"LocalizedFontWeightOverride"); // If any of the font overrides are modified then all of them need to be modified assert(localizedFontWeightOverride != reserved); assert(localizedUITextFontSizeFactorOverride != reserved); assert(localizedUICaptionFontSizeFactorOverride != reserved); m_fontWeightOverride = ParseFontWeight(localizedFontWeightOverride); m_uiTextFontScaleFactorOverride = _wtof(localizedUITextFontSizeFactorOverride->Data()); m_uiCaptionFontScaleFactorOverride = _wtof(localizedUICaptionFontSizeFactorOverride->Data()); } m_fontGroup = ref new LanguageFontGroup(m_language); } FontWeight LocalizationService::ParseFontWeight(String^ fontWeight) { wstring weight = fontWeight->Data(); transform(weight.begin(), weight.end(), weight.begin(), towlower); fontWeight = ref new String(weight.c_str()); if (fontWeight == "black") { return FontWeights::Black; } else if (fontWeight == "bold") { return FontWeights::Bold; } else if (fontWeight == "extrablack") { return FontWeights::ExtraBlack; } else if (fontWeight == "extrabold") { return FontWeights::ExtraBold; } else if (fontWeight == "extralight") { return FontWeights::ExtraLight; } else if (fontWeight == "light") { return FontWeights::Light; } else if (fontWeight == "medium") { return FontWeights::Medium; } else if (fontWeight == "normal") { return FontWeights::Normal; } else if (fontWeight == "semibold") { return FontWeights::SemiBold; } else if (fontWeight == "semilight") { return FontWeights::SemiLight; } else if (fontWeight == "thin") { return FontWeights::Thin; } else { throw invalid_argument("Invalid argument: fontWeight"); } } FlowDirection LocalizationService::GetFlowDirection() { return m_flowDirection; } bool LocalizationService::IsRtlLayout() { return m_flowDirection == FlowDirection::RightToLeft; } String^ LocalizationService::GetLanguage() { return m_language; } bool LocalizationService::GetOverrideFontApiValues() { return m_overrideFontApiValues; } FontFamily^ LocalizationService::GetLanguageFontFamilyForType(LanguageFontType fontType) { if (m_overrideFontApiValues) { return ref new FontFamily(m_fontFamilyOverride); } else { return ref new FontFamily(GetLanguageFont(fontType)->FontFamily); } } LanguageFont^ LocalizationService::GetLanguageFont(LanguageFontType fontType) { assert(!m_overrideFontApiValues); assert(m_fontGroup); switch (fontType) { case LanguageFontType::UIText: return m_fontGroup->UITextFont; case LanguageFontType::UICaption: return m_fontGroup->UICaptionFont; default: throw std::invalid_argument("fontType"); } } String^ LocalizationService::GetFontFamilyOverride() { assert(m_overrideFontApiValues); return m_fontFamilyOverride; } FontWeight LocalizationService::GetFontWeightOverride() { assert(m_overrideFontApiValues); return m_fontWeightOverride; } double LocalizationService::GetFontScaleFactorOverride(LanguageFontType fontType) { assert(m_overrideFontApiValues); switch (fontType) { case LanguageFontType::UIText: return m_uiTextFontScaleFactorOverride; case LanguageFontType::UICaption: return m_uiCaptionFontScaleFactorOverride; default: throw invalid_argument("Invalid argument: fontType"); } } void LocalizationService::OnFontTypePropertyChanged(DependencyObject^ target, LanguageFontType /*oldValue*/, LanguageFontType /*newValue*/) { UpdateFontFamilyAndSize(target); } void LocalizationService::OnFontWeightPropertyChanged(DependencyObject^ target, FontWeight /*oldValue*/, FontWeight /*newValue*/) { UpdateFontFamilyAndSize(target); } void LocalizationService::OnFontSizePropertyChanged(DependencyObject^ target, double /*oldValue*/, double /*newValue*/) { UpdateFontFamilyAndSize(target); } void LocalizationService::UpdateFontFamilyAndSize(DependencyObject^ target) { FontFamily^ fontFamily; FontWeight fontWeight; bool fOverrideFontWeight = false; double scaleFactor; auto service = LocalizationService::GetInstance(); auto fontType = LocalizationService::GetFontType(target); if (service->GetOverrideFontApiValues()) { fontFamily = ref new FontFamily(service->GetFontFamilyOverride()); scaleFactor = service->GetFontScaleFactorOverride(fontType) / 100.0; fontWeight = service->GetFontWeightOverride(); fOverrideFontWeight = true; } else { auto languageFont = service->GetLanguageFont(fontType); fontFamily = ref new FontFamily(languageFont->FontFamily); scaleFactor = languageFont->ScaleFactor / 100.0; } double sizeToUse = LocalizationService::GetFontSize(target) * scaleFactor; auto control = dynamic_cast(target); if (control) { control->FontFamily = fontFamily; if (fOverrideFontWeight) { control->FontWeight = fontWeight; } if (sizeToUse != 0.0) { control->FontSize = sizeToUse; } else { control->ClearValue(Control::FontSizeProperty); } } else { auto textBlock = dynamic_cast(target); if (textBlock) { textBlock->FontFamily = fontFamily; if (fOverrideFontWeight) { textBlock->FontWeight = fontWeight; } if (sizeToUse != 0.0) { textBlock->FontSize = sizeToUse; } else { textBlock->ClearValue(TextBlock::FontSizeProperty); } } else { RichTextBlock^ richTextBlock = dynamic_cast(target); if (richTextBlock) { richTextBlock->FontFamily = fontFamily; if (fOverrideFontWeight) { richTextBlock->FontWeight = fontWeight; } if (sizeToUse != 0.0) { richTextBlock->FontSize = sizeToUse; } else { richTextBlock->ClearValue(RichTextBlock::FontSizeProperty); } } else { TextElement^ textElement = dynamic_cast(target); if (textElement) { textElement->FontFamily = fontFamily; if (fOverrideFontWeight) { textElement->FontWeight = fontWeight; } if (sizeToUse != 0.0) { textElement->FontSize = sizeToUse; } else { textElement->ClearValue(TextElement::FontSizeProperty); } } } } } } // If successful, returns a formatter that respects the user's regional format settings, // as configured by running intl.cpl. DecimalFormatter^ LocalizationService::GetRegionalSettingsAwareDecimalFormatter() { IIterable^ languageIdentifiers = LocalizationService::GetLanguageIdentifiers(); if (languageIdentifiers != nullptr) { return ref new DecimalFormatter(languageIdentifiers, GlobalizationPreferences::HomeGeographicRegion); } return ref new DecimalFormatter(); } // If successful, returns a formatter that respects the user's regional format settings, // as configured by running intl.cpl. // // This helper function creates a DateTimeFormatter with a TwentyFour hour clock DateTimeFormatter^ LocalizationService::GetRegionalSettingsAwareDateTimeFormatter(_In_ String^ format) { IIterable^ languageIdentifiers = LocalizationService::GetLanguageIdentifiers(); if (languageIdentifiers == nullptr) { languageIdentifiers = ApplicationLanguages::Languages; } return ref new DateTimeFormatter(format, languageIdentifiers); } // If successful, returns a formatter that respects the user's regional format settings, // as configured by running intl.cpl. DateTimeFormatter^ LocalizationService::GetRegionalSettingsAwareDateTimeFormatter( _In_ String^ format, _In_ String^ calendarIdentifier, _In_ String^ clockIdentifier) { IIterable^ languageIdentifiers = LocalizationService::GetLanguageIdentifiers(); if (languageIdentifiers == nullptr) { languageIdentifiers = ApplicationLanguages::Languages; } return ref new DateTimeFormatter( format, languageIdentifiers, GlobalizationPreferences::HomeGeographicRegion, calendarIdentifier, clockIdentifier); } CurrencyFormatter^ LocalizationService::GetRegionalSettingsAwareCurrencyFormatter() { String^ userCurrency = (GlobalizationPreferences::Currencies->Size > 0) ? GlobalizationPreferences::Currencies->GetAt(0) : StringReference(DefaultCurrencyCode.data()); IIterable^ languageIdentifiers = LocalizationService::GetLanguageIdentifiers(); if (languageIdentifiers == nullptr) { languageIdentifiers = ApplicationLanguages::Languages; } auto currencyFormatter = ref new CurrencyFormatter( userCurrency, languageIdentifiers, GlobalizationPreferences::HomeGeographicRegion); int fractionDigits = LocalizationSettings::GetInstance().GetCurrencyTrailingDigits(); currencyFormatter->FractionDigits = fractionDigits; return currencyFormatter; } IIterable^ LocalizationService::GetLanguageIdentifiers() { WCHAR currentLocale[LOCALE_NAME_MAX_LENGTH] = {}; int result = GetUserDefaultLocaleName(currentLocale, LOCALE_NAME_MAX_LENGTH); if (result != 0) { // GetUserDefaultLocaleName may return an invalid bcp47 language tag with trailing non-BCP47 friendly characters, // which if present would start with an underscore, for example sort order // (see https://msdn.microsoft.com/en-us/library/windows/desktop/dd373814(v=vs.85).aspx). // Therefore, if there is an underscore in the locale name, trim all characters from the underscore onwards. WCHAR* underscore = wcschr(currentLocale, L'_'); if (underscore != nullptr) { *underscore = L'\0'; } String^ localeString = ref new String(currentLocale); // validate if the locale we have is valid // otherwise we fallback to the default. if (Language::IsWellFormed(localeString)) { auto languageList = ref new Vector(); languageList->Append(localeString); return languageList; } } return nullptr; } unordered_map LocalizationService::GetTokenToReadableNameMap() { // Resources for the engine use numbers as keys. It's inconvenient, but also difficult to // change given that the engine heavily relies on perfect ordering of certain elements. // To compromise, we'll declare a map from engine resource key to automation name from the // standard project resources. static vector> s_parenEngineKeyResourceMap = { // Sine permutations make_pair(L"67", L"SineDegrees"), make_pair(L"73", L"SineRadians"), make_pair(L"79", L"SineGradians"), make_pair(L"70", L"InverseSineDegrees"), make_pair(L"76", L"InverseSineRadians"), make_pair(L"82", L"InverseSineGradians"), make_pair(L"25", L"HyperbolicSine"), make_pair(L"85", L"InverseHyperbolicSine"), // Cosine permutations make_pair(L"68", L"CosineDegrees"), make_pair(L"74", L"CosineRadians"), make_pair(L"80", L"CosineGradians"), make_pair(L"71", L"InverseCosineDegrees"), make_pair(L"77", L"InverseCosineRadians"), make_pair(L"83", L"InverseCosineGradians"), make_pair(L"26", L"HyperbolicCosine"), make_pair(L"86", L"InverseHyperbolicCosine"), // Tangent permutations make_pair(L"69", L"TangentDegrees"), make_pair(L"75", L"TangentRadians"), make_pair(L"81", L"TangentGradians"), make_pair(L"72", L"InverseTangentDegrees"), make_pair(L"78", L"InverseTangentRadians"), make_pair(L"84", L"InverseTangentGradians"), make_pair(L"27", L"HyperbolicTangent"), make_pair(L"87", L"InverseHyperbolicTangent"), // Miscellaneous Scientific functions make_pair(L"94", L"Factorial"), make_pair(L"35", L"DegreeMinuteSecond"), make_pair(L"28", L"NaturalLog"), make_pair(L"91", L"Square") }; static vector> s_noParenEngineKeyResourceMap = { // Programmer mode functions make_pair(L"9", L"LeftShift"), make_pair(L"10", L"RightShift"), // Y Root scientific function make_pair(L"16", L"YRoot") }; unordered_map tokenToReadableNameMap{}; auto resProvider = AppResourceProvider::GetInstance(); static const wstring openParen = resProvider.GetCEngineString(StringReference(s_openParenResourceKey))->Data(); for (const auto& keyPair : s_parenEngineKeyResourceMap) { wstring engineStr = resProvider.GetCEngineString(StringReference(keyPair.first.c_str()))->Data(); wstring automationName = resProvider.GetResourceString(StringReference(keyPair.second.c_str()))->Data(); tokenToReadableNameMap.emplace(engineStr + openParen, automationName); } s_parenEngineKeyResourceMap.clear(); for (const auto& keyPair : s_noParenEngineKeyResourceMap) { wstring engineStr = resProvider.GetCEngineString(StringReference(keyPair.first.c_str()))->Data(); wstring automationName = resProvider.GetResourceString(StringReference(keyPair.second.c_str()))->Data(); tokenToReadableNameMap.emplace(engineStr, automationName); } s_noParenEngineKeyResourceMap.clear(); // Also replace hyphens with "minus" wstring minusText = resProvider.GetResourceString(L"minus")->Data(); tokenToReadableNameMap.emplace(L"-", minusText); return tokenToReadableNameMap; } String^ LocalizationService::GetNarratorReadableToken(String^ rawToken) { static unordered_map s_tokenToReadableNameMap = GetTokenToReadableNameMap(); auto itr = s_tokenToReadableNameMap.find(rawToken->Data()); if (itr == s_tokenToReadableNameMap.end()) { return rawToken; } else { static const String^ openParen = AppResourceProvider::GetInstance().GetCEngineString(StringReference(s_openParenResourceKey)); return ref new String(itr->second.c_str()) + L" " + openParen; } } String^ LocalizationService::GetNarratorReadableString(String^ rawString) { wstringstream readableString{}; readableString << L""; wstring asWstring = rawString->Data(); for (const auto& c : asWstring) { readableString << LocalizationService::GetNarratorReadableToken(L"" + c)->Data(); } return ref new String(readableString.str().c_str()); }