diff --git a/Source/Editor/CustomEditors/Editors/LocalizedStringEditor.cs b/Source/Editor/CustomEditors/Editors/LocalizedStringEditor.cs index 82503d9bc..356b4d664 100644 --- a/Source/Editor/CustomEditors/Editors/LocalizedStringEditor.cs +++ b/Source/Editor/CustomEditors/Editors/LocalizedStringEditor.cs @@ -176,7 +176,6 @@ namespace FlaxEditor.CustomEditors.Editors allKeys.Add(e.Key); } } - _valueElement.TextBox.SetTextAsUser(null); string newKey = null; if (string.IsNullOrEmpty(_idElement.Text)) { @@ -216,17 +215,19 @@ namespace FlaxEditor.CustomEditors.Editors var newValue = _valueElement.Text; Editor.Log(newKey + (newValue != null ? " = " + newValue : string.Empty)); var locales = settings.LocalizedStringTables.GroupBy(x => x.Locale); + var defaultLocale = settings.LocalizedStringTables[0].Locale; foreach (var locale in locales) { var table = locale.First(); var entries = table.Entries; - if (table.Locale == "en") + if (table.Locale == defaultLocale) entries[newKey] = new[] { newValue }; else entries[newKey] = new[] { string.Empty }; table.Entries = entries; table.Save(); } + _valueElement.TextBox.SetTextAsUser(null); _idElement.TextBox.SetTextAsUser(newKey); Profiler.EndEvent(); } diff --git a/Source/Engine/Localization/Localization.cpp b/Source/Engine/Localization/Localization.cpp index e5c035e3a..e34a497b4 100644 --- a/Source/Engine/Localization/Localization.cpp +++ b/Source/Engine/Localization/Localization.cpp @@ -18,6 +18,7 @@ public: CultureInfo CurrentCulture; CultureInfo CurrentLanguage; Array> LocalizedStringTables; + Array> FallbackStringTables; LocalizationService() : EngineService(TEXT("Localization"), -500) @@ -28,6 +29,42 @@ public: void OnLocalizationChanged(); + const String& Get(const String& id, int32 index, const String& fallback) const + { + if (id.IsEmpty()) + return fallback; + + // Try current tables + for (auto& e : LocalizedStringTables) + { + const auto table = e.Get(); + const auto messages = table ? table->Entries.TryGet(id) : nullptr; + if (messages && messages->Count() > index) + return messages->At(index); + } + + // Try fallback tables for current tables + for (auto& e : LocalizedStringTables) + { + const auto table = e.Get(); + const auto fallbackTable = table ? table->FallbackTable.Get() : nullptr; + const auto messages = fallbackTable ? fallbackTable->Entries.TryGet(id) : nullptr; + if (messages && messages->Count() > index) + return messages->At(index); + } + + // Try fallback language tables + for (auto& e : FallbackStringTables) + { + const auto table = e.Get(); + const auto messages = table ? table->Entries.TryGet(id) : nullptr; + if (messages && messages->Count() > index) + return messages->At(index); + } + + return fallback; + } + bool Init() override; }; @@ -46,6 +83,7 @@ void LocalizationSettings::Apply() void LocalizationSettings::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) { DESERIALIZE(LocalizedStringTables); + DESERIALIZE(DefaultFallbackLanguage); } LocalizedString::LocalizedString(const LocalizedString& other) @@ -119,10 +157,12 @@ void LocalizationService::OnLocalizationChanged() PROFILE_CPU(); Instance.LocalizedStringTables.Clear(); + Instance.FallbackStringTables.Clear(); + const StringView en(TEXT("en")); // Collect all localization tables into mapping locale -> tables auto& settings = *LocalizationSettings::Get(); - Dictionary, InlinedAllocation<8>>> tables; + Dictionary, InlinedAllocation<32>>> tables; for (auto& e : settings.LocalizedStringTables) { auto table = e.Get(); @@ -142,9 +182,13 @@ void LocalizationService::OnLocalizationChanged() table = tables.TryGet(parentLanguage.GetName()); if (!table) { - // Fallback to English - const CultureInfo english("en"); - table = tables.TryGet(english.GetName()); + // Fallback to Default + table = tables.TryGet(settings.DefaultFallbackLanguage); + if (!table) + { + // Fallback to English + table = tables.TryGet(en); + } } } @@ -162,6 +206,17 @@ void LocalizationService::OnLocalizationChanged() } LOG(Info, "Using localization for {0}", locale); Instance.LocalizedStringTables.Add(table->Get(), table->Count()); + if (locale != settings.DefaultFallbackLanguage || locale != en) + { + // Cache fallback language tables to support additional text resolving in case of missing entries in the current language + table = tables.TryGet(settings.DefaultFallbackLanguage); + if (!table) + table = tables.TryGet(en); + if (table) + { + Instance.FallbackStringTables.Add(table->Get(), table->Count()); + } + } } #if PLATFORM_ANDROID @@ -193,8 +248,12 @@ void LocalizationService::OnLocalizationChanged() { std::locale::global(std::locale(localeName)); } - catch (std::runtime_error const&) {} - catch (...) {} + catch (std::runtime_error const&) + { + } + catch (...) + { + } } #endif @@ -258,68 +317,12 @@ void Localization::SetCurrentLanguageCulture(const CultureInfo& value) String Localization::GetString(const String& id, const String& fallback) { - const String* result = nullptr; - for (auto& e : Instance.LocalizedStringTables) - { - const auto table = e.Get(); - const auto messages = table ? table->Entries.TryGet(id) : nullptr; - if (messages && messages->Count() != 0) - { - result = &messages->At(0); - break; - } - } - if (!result) - { - // Try using fallback tables - for (auto& e : Instance.LocalizedStringTables) - { - const auto table = e.Get(); - const auto fallbackTable = table ? table->FallbackTable.Get() : nullptr; - const auto messages = fallbackTable ? fallbackTable->Entries.TryGet(id) : nullptr; - if (messages && messages->Count() != 0) - { - result = &messages->At(0); - break; - } - } - } - if (!result) - result = &fallback; - return *result; + return Instance.Get(id, 0, fallback); } String Localization::GetPluralString(const String& id, int32 n, const String& fallback) { - CHECK_RETURN(n >= 1, fallback); - n--; - const String* result = nullptr; - for (auto& e : Instance.LocalizedStringTables) - { - const auto table = e.Get(); - const auto messages = table ? table->Entries.TryGet(id) : nullptr; - if (messages && messages->Count() > n) - { - result = &messages->At(n); - break; - } - } - if (!result) - { - // Try using fallback tables - for (auto& e : Instance.LocalizedStringTables) - { - const auto table = e.Get(); - const auto fallbackTable = table ? table->FallbackTable.Get() : nullptr; - const auto messages = fallbackTable ? fallbackTable->Entries.TryGet(id) : nullptr; - if (messages && messages->Count() > n) - { - result = &messages->At(n); - break; - } - } - } - if (!result) - result = &fallback; - return String::Format(result->GetText(), n); + CHECK_RETURN(n >= 1, String::Format(fallback.GetText(), n)); + const String& format = Instance.Get(id, n - 1, fallback); + return String::Format(format.GetText(), n); } diff --git a/Source/Engine/Localization/Localization.h b/Source/Engine/Localization/Localization.h index 6502c5523..b5998e8f7 100644 --- a/Source/Engine/Localization/Localization.h +++ b/Source/Engine/Localization/Localization.h @@ -10,7 +10,7 @@ /// API_CLASS(Static) class FLAXENGINE_API Localization { -DECLARE_SCRIPTING_TYPE_MINIMAL(Localization); + DECLARE_SCRIPTING_TYPE_MINIMAL(Localization); public: /// /// Gets the current culture (date, time, currency and values formatting locale). diff --git a/Source/Engine/Localization/LocalizationSettings.h b/Source/Engine/Localization/LocalizationSettings.h index 26ba06c96..b62301f1e 100644 --- a/Source/Engine/Localization/LocalizationSettings.h +++ b/Source/Engine/Localization/LocalizationSettings.h @@ -11,7 +11,7 @@ /// API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API LocalizationSettings : public SettingsBase { -DECLARE_SCRIPTING_TYPE_MINIMAL(LocalizationSettings); + DECLARE_SCRIPTING_TYPE_MINIMAL(LocalizationSettings); public: /// /// The list of the string localization tables used by the game. @@ -19,6 +19,12 @@ public: API_FIELD() Array> LocalizedStringTables; + /// + /// The default fallback language to use if localization system fails to pick the system locale language (eg. en-GB). + /// + API_FIELD() + String DefaultFallbackLanguage; + public: /// /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. diff --git a/Source/Engine/Localization/LocalizedString.cs b/Source/Engine/Localization/LocalizedString.cs index 2d6535336..8cdf5e3c8 100644 --- a/Source/Engine/Localization/LocalizedString.cs +++ b/Source/Engine/Localization/LocalizedString.cs @@ -41,7 +41,7 @@ namespace FlaxEngine /// The localized text. public string ToStringPlural(int n) { - return string.IsNullOrEmpty(Value) ? Localization.GetPluralString(Id, n) : Value; + return string.IsNullOrEmpty(Value) ? Localization.GetPluralString(Id, n) : string.Format(Value, n); } /// diff --git a/Source/Engine/Localization/LocalizedStringTable.cpp b/Source/Engine/Localization/LocalizedStringTable.cpp index d7ba24175..781cdec6a 100644 --- a/Source/Engine/Localization/LocalizedStringTable.cpp +++ b/Source/Engine/Localization/LocalizedStringTable.cpp @@ -33,6 +33,28 @@ void LocalizedStringTable::AddPluralString(const StringView& id, const StringVie values[n] = value; } +String LocalizedStringTable::GetString(const String& id) const +{ + StringView result; + const auto messages = Entries.TryGet(id); + if (messages && messages->Count() != 0) + result = messages->At(0); + if (result.IsEmpty() && FallbackTable) + result = FallbackTable->GetString(id); + return result; +} + +String LocalizedStringTable::GetPluralString(const String& id, int32 n) const +{ + StringView result; + const auto messages = Entries.TryGet(id); + if (messages && messages->Count() > n) + result = messages->At(n); + if (result.IsEmpty() && FallbackTable) + result = FallbackTable->GetPluralString(id, n); + return String::Format(result.GetNonTerminatedText(), n); +} + #if USE_EDITOR bool LocalizedStringTable::Save(const StringView& path) diff --git a/Source/Engine/Localization/LocalizedStringTable.h b/Source/Engine/Localization/LocalizedStringTable.h index a9d11b454..e8c56aa64 100644 --- a/Source/Engine/Localization/LocalizedStringTable.h +++ b/Source/Engine/Localization/LocalizedStringTable.h @@ -13,9 +13,8 @@ /// API_CLASS(NoSpawn) class FLAXENGINE_API LocalizedStringTable : public JsonAssetBase { -DECLARE_ASSET_HEADER(LocalizedStringTable); + DECLARE_ASSET_HEADER(LocalizedStringTable); public: - /// /// The locale of the localized string table (eg. pl-PL). /// @@ -32,7 +31,6 @@ public: API_FIELD() Dictionary> Entries; public: - /// /// Adds the localized string to the table. /// @@ -48,6 +46,21 @@ public: /// The plural value (0, 1, 2..). API_FUNCTION() void AddPluralString(const StringView& id, const StringView& value, int32 n); + /// + /// Gets the localized string by using string id lookup. Uses fallback table if text is not included in this table. + /// + /// The message identifier. + /// The localized text. + API_FUNCTION() String GetString(const String& id) const; + + /// + /// Gets the localized plural string by using string id lookup. Uses fallback table if text is not included in this table. + /// + /// The message identifier. + /// The value count for plural message selection. + /// The localized text. + API_FUNCTION() String GetPluralString(const String& id, int32 n) const; + #if USE_EDITOR ///