From 7dd2a8fac95e85aedb7a0627cad5023990ca9399 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 14 Apr 2021 12:22:48 +0200 Subject: [PATCH] Add Localization --- Source/Engine/Engine/Engine.Build.cs | 1 + Source/Engine/Localization/CultureInfo.cpp | 186 ++++++++++++++++++ Source/Engine/Localization/CultureInfo.h | 82 ++++++++ .../Engine/Localization/Localization.Build.cs | 18 ++ Source/Engine/Localization/Localization.cpp | 73 +++++++ Source/Engine/Localization/Localization.h | 44 +++++ Source/ThirdParty/glib.h | 36 ++++ .../Bindings/BindingsGenerator.CSharp.cs | 1 + .../Bindings/BindingsGenerator.Cpp.cs | 6 + 9 files changed, 447 insertions(+) create mode 100644 Source/Engine/Localization/CultureInfo.cpp create mode 100644 Source/Engine/Localization/CultureInfo.h create mode 100644 Source/Engine/Localization/Localization.Build.cs create mode 100644 Source/Engine/Localization/Localization.cpp create mode 100644 Source/Engine/Localization/Localization.h create mode 100644 Source/ThirdParty/glib.h diff --git a/Source/Engine/Engine/Engine.Build.cs b/Source/Engine/Engine/Engine.Build.cs index 38db0e59c..0ee4e7a7c 100644 --- a/Source/Engine/Engine/Engine.Build.cs +++ b/Source/Engine/Engine/Engine.Build.cs @@ -35,6 +35,7 @@ public class Engine : EngineModule options.PublicDependencies.Add("UI"); options.PublicDependencies.Add("Utilities"); options.PublicDependencies.Add("Visject"); + options.PublicDependencies.Add("Localization"); // Use source folder per platform group switch (options.Platform.Target) diff --git a/Source/Engine/Localization/CultureInfo.cpp b/Source/Engine/Localization/CultureInfo.cpp new file mode 100644 index 000000000..b3ece39e8 --- /dev/null +++ b/Source/Engine/Localization/CultureInfo.cpp @@ -0,0 +1,186 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#include "CultureInfo.h" +#include "Engine/Core/Log.h" +#include "Engine/Scripting/ManagedCLR/MType.h" +#include "Engine/Utilities/StringConverter.h" +#if USE_MONO +#include +#include +#include + +typedef struct +{ + MonoObject obj; + MonoBoolean is_read_only; + gint32 lcid; + //... +} MonoCultureInfo; +#endif + +CultureInfo::CultureInfo(int32 lcid) +{ + _lcid = lcid; + _lcidParent = 0; + _data = nullptr; + if (lcid == 0) + return; + if (lcid == 127) + { + _englishName = TEXT("Invariant Culture"); + return; + } +#if USE_MONO + for (int32 i = 0; i < NUM_CULTURE_ENTRIES; i++) + { + auto& e = culture_entries[i]; + if (e.lcid == lcid) + { + _data = (void*)&e; + _lcidParent = (int32)e.parent_lcid; + const char* name = idx2string(e.name); + _name.SetUTF8(name, StringUtils::Length(name)); + const char* nativename = idx2string(e.nativename); + _nativeName.SetUTF8(nativename, StringUtils::Length(nativename)); + const char* englishname = idx2string(e.englishname); + _englishName.SetUTF8(englishname, StringUtils::Length(englishname)); + break; + } + } +#else +#error "Missing CultureInfo implementation." +#endif + if (!_data) + { + LOG(Error, "Unknown LCID {0} for CultureInfo", lcid); + } +} + +CultureInfo::CultureInfo(const StringView& name) + : CultureInfo(StringAnsiView(StringAsANSI<16>(name.Get(), name.Length()).Get(), name.Length())) +{ +} + +CultureInfo::CultureInfo(const StringAnsiView& name) +{ + _data = nullptr; + if (name.IsEmpty()) + { + _lcid = 0; + _lcidParent = 0; + _englishName = TEXT("Invariant Culture"); + return; + } +#if USE_MONO + for (int32 i = 0; i < NUM_CULTURE_ENTRIES; i++) + { + auto& e = culture_entries[i]; + if (name == idx2string(e.name)) + { + _data = (void*)&e; + _lcid = (int32)e.lcid; + _lcidParent = (int32)e.parent_lcid; + _name.SetUTF8(name.Get(), name.Length()); + const char* nativename = idx2string(e.nativename); + _nativeName.SetUTF8(nativename, StringUtils::Length(nativename)); + const char* englishname = idx2string(e.englishname); + _englishName.SetUTF8(englishname, StringUtils::Length(englishname)); + break; + } + } +#else +#error "Missing CultureInfo implementation." +#endif + if (!_data) + { + LOG(Error, "Unknown name {0} for CultureInfo", String(name)); + } +} + +int32 CultureInfo::GetLCID() const +{ + return _lcid; +} + +int32 CultureInfo::GetParentLCID() const +{ + return _lcidParent; +} + +const String& CultureInfo::GetName() const +{ + return _name; +} + +const String& CultureInfo::GetNativeName() const +{ + return _nativeName; +} + +const String& CultureInfo::GetEnglishName() const +{ + return _englishName; +} + +bool CultureInfo::IsRightToLeft() const +{ +#if USE_MONO + const auto data = static_cast(_data); + if (data) + return data->text_info.is_right_to_left ? true : false; +#else +#error "Missing CultureInfo implementation." +#endif + return false; +} + +String CultureInfo::ToString() const +{ + return _name; +} + +bool CultureInfo::operator==(const CultureInfo& other) const +{ + return _lcid == other._lcid; +} + +void* MUtils::ToManaged(const CultureInfo& value) +{ +#if USE_MONO + const auto klass = mono_class_from_name(mono_get_corlib(), "System.Globalization", "CultureInfo"); + if (klass) + { + void* iter = nullptr; + MonoMethod* method; + while ((method = mono_class_get_methods(klass, &iter))) + { + if (StringUtils::Compare(mono_method_get_name(method), ".ctor") == 0) + { + const auto sig = mono_method_signature(method); + void* sigParams = nullptr; + mono_signature_get_params(sig, &sigParams); + if (mono_signature_get_param_count(sig) == 1 && mono_type_get_class(((MonoType**)sigParams)[0]) != mono_get_string_class()) + { + MonoObject* obj = mono_object_new(mono_domain_get(), klass); + int32 lcid = value.GetLCID(); + void* params[1]; + params[0] = &lcid; + mono_runtime_invoke(method, obj, params, nullptr); + return obj; + } + } + } + } +#endif + return nullptr; +} + +CultureInfo MUtils::ToNative(void* value) +{ + int32 lcid = 127; +#if USE_MONO + if (value) + lcid = static_cast(value)->lcid; +#endif + return CultureInfo(lcid); +} diff --git a/Source/Engine/Localization/CultureInfo.h b/Source/Engine/Localization/CultureInfo.h new file mode 100644 index 000000000..7a156e2dd --- /dev/null +++ b/Source/Engine/Localization/CultureInfo.h @@ -0,0 +1,82 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Types/String.h" +#include "Engine/Core/Formatting.h" + +/// +/// The information about a specific culture (aka locale). The information includes the names for the culture, the writing system, the calendar used, the sort order of strings, and formatting for dates and numbers. +/// +API_CLASS(InBuild, Namespace="System.Globalization") class FLAXENGINE_API CultureInfo +{ +DECLARE_SCRIPTING_TYPE_MINIMAL(CultureInfo); +private: + void* _data; + int32 _lcid; + int32 _lcidParent; + String _name; + String _nativeName; + String _englishName; + +public: + /// + /// Initializes a new instance of the class. + /// + /// The culture identifier. + CultureInfo(int32 lcid); + + /// + /// Initializes a new instance of the class. + /// + /// The culture name (eg. pl-PL). + CultureInfo(const StringView& name); + + /// + /// Initializes a new instance of the class. + /// + /// The culture name (eg. pl-PL). + CultureInfo(const StringAnsiView& name); + +public: + /// + /// Gets the culture identifier. + /// + int32 GetLCID() const; + + /// + /// Gets the parent culture identifier. + /// + int32 GetParentLCID() const; + + /// + /// Gets the culture name (eg. pl-PL). + /// + const String& GetName() const; + + /// + /// Gets the full localized culture name (eg. Polish (Poland)). + /// + const String& GetNativeName() const; + + /// + /// Gets the culture name in English (eg. polski (Polska)). + /// + const String& GetEnglishName() const; + + /// + /// Returns true if culture uses right-to-left (RTL) text layout. Otherwise it's more common left-to-right. + /// + bool IsRightToLeft() const; + + String ToString() const; + bool operator==(const CultureInfo& other) const; +}; + +DEFINE_DEFAULT_FORMATTING_VIA_TO_STRING(CultureInfo); + +namespace MUtils +{ + extern void* ToManaged(const CultureInfo& value); + extern CultureInfo ToNative(void* value); +} diff --git a/Source/Engine/Localization/Localization.Build.cs b/Source/Engine/Localization/Localization.Build.cs new file mode 100644 index 000000000..b7b839bcc --- /dev/null +++ b/Source/Engine/Localization/Localization.Build.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +using Flax.Build; +using Flax.Build.NativeCpp; + +/// +/// Localization and internalization module. +/// +public class Localization : EngineModule +{ + /// + public override void Setup(BuildOptions options) + { + base.Setup(options); + + options.PublicDependencies.Add("Scripting"); + } +} diff --git a/Source/Engine/Localization/Localization.cpp b/Source/Engine/Localization/Localization.cpp new file mode 100644 index 000000000..691e4ad80 --- /dev/null +++ b/Source/Engine/Localization/Localization.cpp @@ -0,0 +1,73 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#include "Localization.h" +#include "CultureInfo.h" +#include "Engine/Core/Log.h" +#include "Engine/Engine/EngineService.h" + +class LocalizationService : public EngineService +{ +public: + LocalizationService() + : EngineService(TEXT("Localization"), -950) + { + } + + bool Init() override; +}; + +namespace +{ + CultureInfo CurrentCulture(0); + CultureInfo CurrentLanguage(0); + LocalizationService LocalizationServiceInstance; +} + +bool LocalizationService::Init() +{ + CurrentLanguage = CurrentCulture = CultureInfo(Platform::GetUserLocaleName()); + return false; +} + +Delegate<> Localization::CurrentLanguageCultureChanged; + +const CultureInfo& Localization::GetCurrentCulture() +{ + return CurrentCulture; +} + +void Localization::SetCurrentCulture(const CultureInfo& value) +{ + if (CurrentCulture == value) + return; + + LOG(Info, "Changing current culture to: {0} ({1})", value.GetName(), value.GetLCID()); + CurrentCulture = value; + CurrentLanguageCultureChanged(); +} + +const CultureInfo& Localization::GetCurrentLanguage() +{ + return CurrentLanguage; +} + +void Localization::SetCurrentLanguage(const CultureInfo& value) +{ + if (CurrentLanguage == value) + return; + + LOG(Info, "Changing current language to: {0} ({1})", value.GetName(), value.GetLCID()); + CurrentLanguage = value; + CurrentLanguageCultureChanged(); +} + +void Localization::SetCurrentLanguageCulture(const CultureInfo& value) +{ + if (CurrentCulture == value && CurrentLanguage == value) + return; + + LOG(Info, "Changing current language and culture to: {0} ({1})", value.GetName(), value.GetLCID()); + CurrentCulture = value; + CurrentLanguage = value; + CurrentLanguageCultureChanged(); +} diff --git a/Source/Engine/Localization/Localization.h b/Source/Engine/Localization/Localization.h new file mode 100644 index 000000000..7d47c0b69 --- /dev/null +++ b/Source/Engine/Localization/Localization.h @@ -0,0 +1,44 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#pragma once + +#include "CultureInfo.h" +#include "Engine/Core/Types/BaseTypes.h" + +/// +/// The language and culture localization manager. +/// +API_CLASS(Static) class FLAXENGINE_API Localization +{ +DECLARE_SCRIPTING_TYPE_MINIMAL(Localization); +public: + /// + /// Gets the current culture (date, time, currency and values formatting locale). + /// + API_PROPERTY() static const CultureInfo& GetCurrentCulture(); + + /// + /// Sets the current culture (date, time, currency and values formatting locale). + /// + API_PROPERTY() static void SetCurrentCulture(const CultureInfo& value); + + /// + /// Gets the current language (text display locale). + /// + API_PROPERTY() static const CultureInfo& GetCurrentLanguage(); + + /// + /// Sets the current language (text display locale). + /// + API_PROPERTY() static void SetCurrentLanguage(const CultureInfo& value); + + /// + /// Sets both the current language (text display locale) and the current culture (date, time, currency and values formatting locale) at once. + /// + API_FUNCTION() static void SetCurrentLanguageCulture(const CultureInfo& value); + + /// + /// Occurs when current culture or language gets changed. Can be used to refresh UI to reflect language changes. + /// + API_EVENT() static Delegate<> CurrentLanguageCultureChanged; +}; diff --git a/Source/ThirdParty/glib.h b/Source/ThirdParty/glib.h new file mode 100644 index 000000000..a8460bbe9 --- /dev/null +++ b/Source/ThirdParty/glib.h @@ -0,0 +1,36 @@ +// Wrapper for mono/mono/eglib/glib.h to mock the types for embedding + +#ifndef _GLIB_H_ +#define _GLIB_H_ + +#include +#include + +/* + * Basic data types + */ +typedef int gint; +typedef unsigned int guint; +typedef short gshort; +typedef unsigned short gushort; +typedef long glong; +typedef unsigned long gulong; +typedef void * gpointer; +typedef const void * gconstpointer; +typedef char gchar; +typedef unsigned char guchar; + +/* Types defined in terms of the stdint.h */ +typedef int8_t gint8; +typedef uint8_t guint8; +typedef int16_t gint16; +typedef uint16_t guint16; +typedef int32_t gint32; +typedef uint32_t guint32; +typedef int64_t gint64; +typedef uint64_t guint64; +typedef float gfloat; +typedef double gdouble; +typedef int32_t gboolean; + +#endif diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs index ed07e1abd..4c0120ca3 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs @@ -1059,6 +1059,7 @@ namespace Flax.Build.Bindings // Using declarations contents.AppendLine("using System;"); contents.AppendLine("using System.ComponentModel;"); + contents.AppendLine("using System.Globalization;"); // TODO: using declarations based on actual types usage contents.AppendLine("using System.Runtime.CompilerServices;"); contents.AppendLine("using System.Runtime.InteropServices;"); foreach (var e in moduleInfo.Children) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 424286e0e..738592422 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -307,6 +307,9 @@ namespace Flax.Build.Bindings case "MClass": type = "MonoReflectionType*"; return "MUtils::GetType({0})"; + case "CultureInfo": + type = "void*"; + return "MUtils::ToManaged({0})"; default: var apiType = FindApiTypeInfo(buildData, typeInfo, caller); if (apiType != null) @@ -448,6 +451,9 @@ namespace Flax.Build.Bindings case "VariantType": type = "MonoReflectionType*"; return "MUtils::UnboxVariantType({0})"; + case "CultureInfo": + type = "void*"; + return "MUtils::ToNative({0})"; default: // ScriptingObjectReference or AssetReference or WeakAssetReference or SoftObjectReference if ((typeInfo.Type == "ScriptingObjectReference" || typeInfo.Type == "AssetReference" || typeInfo.Type == "WeakAssetReference" || typeInfo.Type == "SoftObjectReference") && typeInfo.GenericArgs != null)