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));
}
}
}