using System; using System.Collections.Generic; using System.Globalization; using UnityEngine.Localization.Metadata; using UnityEngine.Localization.Pseudo; using UnityEngine.Localization.Settings; using UnityEngine.Pool; namespace UnityEngine.Localization { /// /// Represents identification information for a language or its regional variant. /// Also includes access to the CultureInfo which provides culture-specific instances /// of the DateTimeFormatInfo, NumberFormatInfo, CompareInfo, and TextInfo objects. /// /// /// This example shows the various ways to create a LocaleIdentifier. /// /// /// /// This shows how to create a Locale for English and a regional Locale for English(UK). /// /// [Serializable] public struct LocaleIdentifier : IEquatable, IComparable { [SerializeField] string m_Code; CultureInfo m_CultureInfo; /// /// The culture name in the format [language]-[region]. /// The name is a combination of an ISO 639 two-letter lowercase culture code associated with a language and an ISO 3166 /// two-letter uppercase subculture code associated with a country or region. /// For example, Language English would be 'en', Regional English(UK) would be 'en-GB' and Regional English(US) would be 'en-US'. /// It is possible to use any string value when representing a non-standard identifier. /// public string Code => m_Code; /// /// A [CultureInfo](https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo) representation of the Locale. /// The is used to query for a valid [CultureInfo}(https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo). /// If a value can not be determined from the then will be returned. /// /// /// This example shows how the CultureInfo can be retrieved after creating a LocaleIdentifier using a Code. /// /// public CultureInfo CultureInfo { get { if (m_CultureInfo == null && !string.IsNullOrEmpty(m_Code)) { try { m_CultureInfo = CultureInfo.GetCultureInfo(m_Code); } catch (CultureNotFoundException) { // If a culture info can not be found then we do not consider this an error. It could be a custom locale. } } return m_CultureInfo; } } /// /// Create a LocaleIdentifier from a culture code string. /// /// public LocaleIdentifier(string code) { m_Code = code; m_CultureInfo = null; } /// /// Create a LocaleIdentifier from a CultureInfo instance. /// /// /// Thrown if the culture is null. public LocaleIdentifier(CultureInfo culture) { if (culture == null) throw new ArgumentNullException(nameof(culture)); m_Code = culture.Name; m_CultureInfo = culture; } /// /// Create a LocaleIdentifier from a [SystemLanguage](https://docs.unity3d.com/ScriptReference/SystemLanguage.html) enum value. /// /// public LocaleIdentifier(SystemLanguage systemLanguage) : this(SystemLanguageConverter.GetSystemLanguageCultureCode(systemLanguage)) { } #pragma warning disable CA2225 // CA2225: Operator overloads have named alternates /// /// Create a LocaleIdentifier from a culture code string. /// /// /// public static implicit operator LocaleIdentifier(string code) => new LocaleIdentifier(code); /// /// Create a LocaleIdentifier from a CultureInfo instance. /// /// /// Thrown if the culture is null. /// public static implicit operator LocaleIdentifier(CultureInfo culture) => new LocaleIdentifier(culture); /// /// Create a LocaleIdentifier from a [SystemLanguage](https://docs.unity3d.com/ScriptReference/SystemLanguage.html) enum value. /// /// /// public static implicit operator LocaleIdentifier(SystemLanguage systemLanguage) => new LocaleIdentifier(systemLanguage); #pragma warning restore CA2225 /// /// Returns a string representation. /// /// public override string ToString() { if (string.IsNullOrEmpty(m_Code)) { return "undefined"; } return $"{(CultureInfo != null ? CultureInfo.EnglishName : "Custom")}({Code})"; } /// /// Compare the LocaleIdentifier to another LocaleIdentifier. /// /// /// public override bool Equals(object obj) { if (obj is null) return false; return obj is LocaleIdentifier identifier && Equals(identifier); } /// /// Compare the LocaleIdentifier to another LocaleIdentifier. /// /// /// public bool Equals(LocaleIdentifier other) { // Treat null and empty as the same if (string.IsNullOrEmpty(other.Code) && string.IsNullOrEmpty(Code)) return true; return string.Equals(Code, other.Code, StringComparison.OrdinalIgnoreCase); } /// /// Returns the hash code of [CultureInfo}(https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo) or if it is null. /// /// public override int GetHashCode() { return !string.IsNullOrEmpty(Code) ? Code.GetHashCode() : base.GetHashCode(); } /// /// Compare to another . /// Performs a comparison against [CultureInfo.EnglishName](https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.englishname) property. /// /// Value to compare against. /// public int CompareTo(LocaleIdentifier other) { if (CultureInfo == null || other.CultureInfo == null) return 1; return string.CompareOrdinal(CultureInfo.EnglishName, other.CultureInfo.EnglishName); } /// /// Compare the LocaleIdentifier to another LocaleIdentifier. /// /// /// /// public static bool operator==(LocaleIdentifier l1, LocaleIdentifier l2) => l1.Equals(l2); /// /// Compare the LocaleIdentifier to another LocaleIdentifier. /// /// /// /// public static bool operator!=(LocaleIdentifier l1, LocaleIdentifier l2) => !l1.Equals(l2); } /// /// A Locale represents a language. It supports regional variations and can be configured with an optional fallback Locale via metadata. /// public class Locale : ScriptableObject, IEquatable, IComparable, ISerializationCallbackReceiver { [SerializeField] LocaleIdentifier m_Identifier; [SerializeField] [MetadataType(MetadataType.Locale)] MetadataCollection m_Metadata = new MetadataCollection(); [SerializeField] string m_LocaleName; [SerializeField] string m_CustomFormatCultureCode; [SerializeField] bool m_UseCustomFormatter; [SerializeField] ushort m_SortOrder = 10000; // Default to a large value so new Locales are always at the end of a list. IFormatProvider m_Formatter; /// /// The identifier contains the identifying information such as the id and culture Code for this Locale. /// public LocaleIdentifier Identifier { get => m_Identifier; set => m_Identifier = value; } /// /// Optional Metadata. It is possible to attach additional data to the Locale providing /// it implements the interface and is serializable. /// public MetadataCollection Metadata { get => m_Metadata; set => m_Metadata = value; } /// /// The sort order can be used to override the order of Locales when sorted in a list. /// If Locales both have the same SortOrder then they will be sorted by name. /// public ushort SortOrder { get => m_SortOrder; set => m_SortOrder = value; } /// /// The name of the Locale. /// This can be used to customize how the Locale name should be presented to the user, such as in a language selection menu. /// public string LocaleName { get { if (!string.IsNullOrEmpty(m_LocaleName)) return m_LocaleName; if (Identifier.CultureInfo != null) return Identifier.CultureInfo.EnglishName; return name; } set => m_LocaleName = value; } /// /// Returns the first fallback locale or if one does not exist or it could not be found. /// /// The fallback locale or . [Obsolete("GetFallback is obsolete, please use GetFallbacks.")] public virtual Locale GetFallback() => GetFallbacks().GetEnumerator().Current; /// /// Returns the fallbacks in order or priority. If the locale does not contain any metadata then the CultureInfo will be used to find a fallback. /// /// The fallback locale or . public IEnumerable GetFallbacks() { if (Metadata == null) yield break; // Ensure we only return each locale once. using (HashSetPool.Get(out var processedLocales)) { var entries = Metadata.MetadataEntries; for (int i = 0; i < entries.Count; ++i) { if (entries[i] is FallbackLocale fallbackLocale) { if (fallbackLocale.Locale != null && !processedLocales.Contains(fallbackLocale.Locale)) { processedLocales.Add(fallbackLocale.Locale); yield return fallbackLocale.Locale; } } } // If we did not find any then revert to using the culture info fallback data. if (processedLocales.Count == 0) { Locale fallBack = null; var cultureInfo = Identifier.CultureInfo; if (cultureInfo != null) { while (cultureInfo != CultureInfo.InvariantCulture && fallBack == null) { var fb = LocalizationSettings.AvailableLocales.GetLocale(cultureInfo); if (fb != this) fallBack = fb; cultureInfo = cultureInfo.Parent; } } if (fallBack != null) yield return fallBack; } } } /// /// When , will be used for any culture sensitive formatting instead of . /// public bool UseCustomFormatter { get => m_UseCustomFormatter; set { m_UseCustomFormatter = value; m_Formatter = null; } } /// /// The Language code to use when applying any culture specific string formatting, such as date, time, currency. /// By default, the Code will be used however this field can be used to override this such as when you /// are using a custom Locale which has no known formatter. /// public string CustomFormatterCode { get => m_CustomFormatCultureCode; set { m_CustomFormatCultureCode = value; m_Formatter = null; } } /// /// The Formatter that will be applied to any Smart Strings for this Locale. /// By default, the [CultureInfo](https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo) will be used when is not set. /// public virtual IFormatProvider Formatter { get { if (m_Formatter == null) m_Formatter = GetFormatter(UseCustomFormatter, Identifier, CustomFormatterCode); return m_Formatter; } set => m_Formatter = value; } internal static CultureInfo GetFormatter(bool useCustom, LocaleIdentifier localeIdentifier, string customCode) { CultureInfo cultureInfo = null; if (useCustom) cultureInfo = string.IsNullOrEmpty(customCode) ? CultureInfo.InvariantCulture : new LocaleIdentifier(customCode).CultureInfo; if (cultureInfo == null) cultureInfo = localeIdentifier.CultureInfo; return cultureInfo; } /// /// Create a new using the culture code. /// /// Culture code. /// public static Locale CreateLocale(string code) { var locale = CreateInstance(); locale.m_Identifier = new LocaleIdentifier(code); if (locale.m_Identifier.CultureInfo != null) { locale.name = locale.m_Identifier.CultureInfo.EnglishName; } return locale; } /// /// Create a new using the provided . /// /// /// public static Locale CreateLocale(LocaleIdentifier identifier) { var locale = CreateInstance(); locale.m_Identifier = identifier; if (locale.m_Identifier.CultureInfo != null) { locale.LocaleName = locale.m_Identifier.CultureInfo.EnglishName; } return locale; } /// /// Create a using the system language enum value. /// /// /// public static Locale CreateLocale(SystemLanguage language) { return CreateLocale(new LocaleIdentifier(SystemLanguageConverter.GetSystemLanguageCultureCode(language))); } /// /// Create a using a CultureInfo. /// /// /// public static Locale CreateLocale(CultureInfo cultureInfo) { return CreateLocale(new LocaleIdentifier(cultureInfo)); } /// /// Compares the Locales properties. /// First the sort orders are compared, if they are the same then the will be considered instead. /// /// /// public int CompareTo(Locale other) { if (other == null) return -1; // Sort by the sort order if they are different if (SortOrder != other.SortOrder) { return SortOrder.CompareTo(other.SortOrder); } // If they are both the same type then use the name to sort if (GetType() == other.GetType()) { var result = String.CompareOrdinal(LocaleName, other.LocaleName); return result != 0 ? result : GetInstanceID().CompareTo(other.GetInstanceID()); } // Normal Locale's go before PseudoLocale's if (other is PseudoLocale) return -1; return 1; } public void OnAfterDeserialize() { m_Formatter = null; } public void OnBeforeSerialize() { if (string.IsNullOrEmpty(m_LocaleName)) m_LocaleName = name; } /// /// Returns or name if it is null or empty. /// /// public override string ToString() => string.IsNullOrEmpty(LocaleName) ? name : LocaleName; /// /// Compares the Locale properties. /// /// /// public bool Equals(Locale other) { if (other == null) return false; return LocaleName == other.LocaleName && Identifier.Equals((other.Identifier)); } } }