2025-05-01 01:48:08 -07:00

723 lines
29 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Localization.Metadata;
using UnityEngine.Localization.Operations;
using UnityEngine.Pool;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace UnityEngine.Localization.Settings
{
/// <summary>
/// The localization settings is the core component to the localization system.
/// It provides the entry point to all player based localization features.
/// </summary>
public class LocalizationSettings : ScriptableObject, IReset, IDisposable
{
/// <summary>
/// The name to use when retrieving the LocalizationSettings from CustomObject API.
/// </summary>
internal const string ConfigName = "com.unity.localization.settings";
internal const string ConfigEditorLocale = "com.unity.localization-edit-locale";
// Used when faking an empty project
internal const string IgnoreSettings = "IgnoreSettings";
internal const string LocaleLabel = "Locale";
internal const string PreloadLabel = "Preload";
[SerializeReference]
List<IStartupLocaleSelector> m_StartupSelectors = new List<IStartupLocaleSelector>
{
new CommandLineLocaleSelector(),
new SystemLocaleSelector(),
new SpecificLocaleSelector()
};
[SerializeReference]
ILocalesProvider m_AvailableLocales = new LocalesProvider();
[SerializeReference]
LocalizedAssetDatabase m_AssetDatabase = new LocalizedAssetDatabase();
[SerializeReference]
LocalizedStringDatabase m_StringDatabase = new LocalizedStringDatabase();
[MetadataType(MetadataType.LocalizationSettings)]
[SerializeField]
MetadataCollection m_Metadata = new MetadataCollection();
[SerializeField]
internal LocaleIdentifier m_ProjectLocaleIdentifier = "en";
[SerializeField]
PreloadBehavior m_PreloadBehavior = PreloadBehavior.PreloadSelectedLocale;
[SerializeField]
bool m_InitializeSynchronously;
internal AsyncOperationHandle<LocalizationSettings> m_InitializingOperationHandle;
AsyncOperationHandle<Locale> m_SelectedLocaleAsync;
Locale m_ProjectLocale;
CallbackArray<Action<Locale>> m_SelectedLocaleChanged;
internal bool IsChangingSelectedLocale { get; private set; }
internal static LocalizationSettings s_Instance;
/// <summary>
/// Called when the <see cref="SelectedLocale"/> is changed.
/// This will be called after <see cref="InitializationOperation"/> is completed so any preloading operations will be finished.
/// </summary>
/// <summary>
/// Returns <see langword="true"/> if <seealso cref="OnSelectedLocaleChanged"/> has any subscribers.
/// </summary>
internal bool HasSelectedLocaleChangedSubscribers => m_SelectedLocaleChanged.Length != 0;
/// <summary>
/// Called when the <see cref="SelectedLocale"/> is changed.
/// This will be called after <see cref="InitializationOperation"/> is completed so any preloading operations will be finished.
/// </summary>
public event Action<Locale> OnSelectedLocaleChanged
{
add => m_SelectedLocaleChanged.Add(value);
remove => m_SelectedLocaleChanged.RemoveByMovingTail(value);
}
/// <summary>
/// Indicates if there is a LocalizationSettings present. If one is not found then it will attempt to find one however
/// unlike <see cref="Instance"/> it will not create a default, if one can not be found.
/// </summary>
/// <value><see langword="true"/> if has settings; otherwise, <see langword="false"/>.</value>
public static bool HasSettings
{
get
{
// Use ReferenceEquals so we dont get false positives when using MoQ
if (ReferenceEquals(s_Instance, null))
s_Instance = GetInstanceDontCreateDefault();
return !ReferenceEquals(s_Instance, null);
}
}
/// <summary>
/// The localization system may not be immediately ready. Loading Locales, preloading assets etc.
/// This operation can be used to check when the system is ready. You can yield on this in a coroutine to wait.
/// If <see cref="InitializeSynchronously"/> is <see langword="true"/> then this operation will complete synchronously the first time it is called.
/// Uses [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) to force the loading to complete synchronously.
/// Please note that [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) is not supported on
/// [WebGL](https://docs.unity3d.com/Packages/com.unity.addressables@latest/index.html?subfolder=/manual/SynchronousAddressables.html#webgl) and <see cref="InitializeSynchronously"/>
/// will be ignored when running on WebGL.
/// </summary>
/// <example>
/// This shows how to use a coroutine to wait for the Initialization Operation to complete.
/// <code source="../../DocCodeSamples.Tests/LocalizationSettingsSamples.cs" region="asynchronous"/>
/// </example>
/// <example>
/// This shows how to use the <see cref="AsyncOperationHandle{TObject}.Completed"/> event to get a callback when the Initialization Operation is complete.
/// <code source="../../DocCodeSamples.Tests/LocalizationSettingsSamples.cs" region="asynchronous-event"/>
/// </example>
/// <example>
/// This shows how to force the Initialization Operation to complete synchronously using [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion).
/// Note [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) is not supported on WebGL.
/// <code source="../../DocCodeSamples.Tests/LocalizationSettingsSamples.cs" region="synchronous"/>
/// </example>
public static AsyncOperationHandle<LocalizationSettings> InitializationOperation => Instance.GetInitializationOperation();
/// <summary>
/// Singleton instance for the Localization Settings.
/// </summary>
public static LocalizationSettings Instance
{
get
{
// Use ReferenceEquals so we dont get false positives when using MoQ
if (ReferenceEquals(s_Instance, null))
s_Instance = GetOrCreateSettings();
return s_Instance;
}
set => s_Instance = value;
}
/// <summary>
/// <inheritdoc cref="IStartupLocaleSelector"/>
/// </summary>
public static List<IStartupLocaleSelector> StartupLocaleSelectors => Instance.GetStartupLocaleSelectors();
/// <summary>
/// <inheritdoc cref="AvailableLocales"/>
/// </summary>
public static ILocalesProvider AvailableLocales
{
get => Instance.GetAvailableLocales();
set => Instance.SetAvailableLocales(value);
}
/// <summary>
/// The asset database is responsible for providing localized assets.
/// </summary>
public static LocalizedAssetDatabase AssetDatabase
{
get => Instance.GetAssetDatabase();
set => Instance.SetAssetDatabase(value);
}
/// <summary>
/// The string database is responsible for providing localized string assets.
/// </summary>
public static LocalizedStringDatabase StringDatabase
{
get => Instance.GetStringDatabase();
set => Instance.SetStringDatabase(value);
}
/// <summary>
/// Returns the Localization Settings Metadata.
/// Metadata can be used to contain additional information such as App Name localization settings.
/// </summary>
public static MetadataCollection Metadata => Instance.GetMetadata();
/// <summary>
/// The current selected <see cref="Locale"/>. This is the Locale that will be used by default when localizing assets and strings.
/// Calling this when the Localization system has not initialized will force the Localization system to load all Locales before returning,
/// see <see cref="SelectedLocaleAsync"/> for a version that will load the Locales asynchronously.
/// </summary>
public static Locale SelectedLocale
{
get => Instance.GetSelectedLocale();
set => Instance.SetSelectedLocale(value);
}
/// <summary>
/// The current selected Locale. This is the Locale that will be used by default when localizing assets and strings.
/// If <see cref="InitializationOperation"/> has not been completed yet then this will wait for the <see cref="AvailableLocales"/> part to complete first.
/// It will not wait for the entire <see cref="InitializationOperation"/> but just the part that initializes the Locales.
/// See <seealso cref="SelectedLocale"/> for a synchronous version that will block until the Locales have been loaded.
/// </summary>
public static AsyncOperationHandle<Locale> SelectedLocaleAsync => Instance.GetSelectedLocaleAsync();
/// <summary>
/// Event that is sent when the <see cref="SelectedLocale"/> is changed.
/// </summary>
/// <example>
/// This shows how to keep track of the current selected <see cref="Locale"/>.
/// <code source="../../DocCodeSamples.Tests/LocalizationSettingsSamples.cs" region="selected-locale-changed"/>
/// </example>
public static event Action<Locale> SelectedLocaleChanged
{
add => Instance.OnSelectedLocaleChanged += value;
remove => Instance.OnSelectedLocaleChanged -= value;
}
/// <summary>
/// When tracking property variants in a scene, any changes you make whilst in this Locale are saved into the source object instead of as a variant.
/// </summary>
public static Locale ProjectLocale
{
get
{
if (Instance.m_ProjectLocale == null || Instance.m_ProjectLocale.Identifier != Instance.m_ProjectLocaleIdentifier)
Instance.m_ProjectLocale = AvailableLocales?.GetLocale(Instance.m_ProjectLocaleIdentifier);
return Instance.m_ProjectLocale;
}
set
{
Instance.m_ProjectLocale = value;
Instance.m_ProjectLocaleIdentifier = value != null ? value.Identifier : default;
}
}
/// <summary>
/// Forces the <see cref="InitializationOperation"/> to complete immediately when it is started.
/// Uses [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) to force the loading to complete synchronously.
/// Please note that [WaitForCompletion](xref:UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle.WaitForCompletion) is not supported on
/// [WebGL](https://docs.unity3d.com/Packages/com.unity.addressables@latest/index.html?subfolder=/manual/SynchronousAddressables.html#webgl) and <see cref="InitializeSynchronously"/>
/// will be ignored when running on WebGL.
/// </summary>
public static bool InitializeSynchronously
{
get => Instance.m_InitializeSynchronously;
set => Instance.m_InitializeSynchronously = value;
}
/// <summary>
/// Determines which tables, that have been marked as preload, will be loaded during the preloading step.
/// </summary>
public static PreloadBehavior PreloadBehavior
{
get => Instance.m_PreloadBehavior;
set => Instance.m_PreloadBehavior = value;
}
internal virtual void OnEnable()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.playModeStateChanged += EditorApplication_playModeStateChanged;
if (UnityEditor.SessionState.GetBool(IgnoreSettings, false))
{
return;
}
#endif
if (s_Instance == null)
{
s_Instance = this;
}
}
#if UNITY_EDITOR
void EditorApplication_playModeStateChanged(UnityEditor.PlayModeStateChange obj)
{
if (obj == UnityEditor.PlayModeStateChange.ExitingEditMode || obj == UnityEditor.PlayModeStateChange.ExitingPlayMode)
{
ResetState();
}
}
void OnDisable()
{
ResetState();
UnityEditor.EditorApplication.playModeStateChanged -= EditorApplication_playModeStateChanged;
}
#endif
/// <summary>
/// Used to ensure a <see cref="LocalizationSettings"/> exists in the project.
/// Throws an exception if <see cref="HasSettings"/> is false.
/// </summary>
/// <param name="error"></param>
internal static void ValidateSettingsExist(string error = "")
{
if (!HasSettings)
{
throw new Exception($"There is no active LocalizationSettings.\n {error}");
}
}
/// <summary>
/// <inheritdoc cref="InitializationOperation"/>
/// </summary>
/// <returns></returns>
public virtual AsyncOperationHandle<LocalizationSettings> GetInitializationOperation()
{
if (!m_InitializingOperationHandle.IsValid())
{
var operation = Operations.InitializationOperation.Pool.Get();
operation.Init(this);
// We need to depend on InitializeAsync to workaround an issue (LOC-823)
operation.Dependency = AddressablesInterface.Instance.InitializeAddressablesAsync();
m_InitializingOperationHandle = AddressablesInterface.ResourceManager.StartOperation(operation, operation.Dependency);
#if !UNITY_WEBGL // WebGL does not support WaitForCompletion
if (!m_InitializingOperationHandle.IsDone && m_InitializeSynchronously && IsPlaying)
m_InitializingOperationHandle.WaitForCompletion();
#endif
}
return m_InitializingOperationHandle;
}
#if UNITY_EDITOR
/// <summary>
/// We use this for testing so we don't have to enter play mode.
/// </summary>
internal bool? IsPlayingOverride { get; set; }
#endif
internal bool IsPlayingOrWillChangePlaymode
{
get
{
#if UNITY_EDITOR
if (IsPlayingOverride.HasValue)
return IsPlayingOverride.Value;
return UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || IsPlaying;
#else
return true;
#endif
}
}
internal bool IsPlaying
{
get
{
#if UNITY_EDITOR
if (IsPlayingOverride.HasValue)
return IsPlayingOverride.Value;
#endif
return Application.isPlaying;
}
}
internal virtual RuntimePlatform Platform => Application.platform;
/// <summary>
/// <inheritdoc cref="IStartupLocaleSelector"/>
/// </summary>
/// <returns>\</returns>
public List<IStartupLocaleSelector> GetStartupLocaleSelectors() => m_StartupSelectors;
/// <summary>
/// <inheritdoc cref="AvailableLocales"/>
/// </summary>
/// <param name="available"></param>
public void SetAvailableLocales(ILocalesProvider available) => m_AvailableLocales = available;
/// <summary>
/// <inheritdoc cref="AvailableLocales"/>
/// </summary>
/// <returns>\</returns>
public virtual ILocalesProvider GetAvailableLocales() => m_AvailableLocales;
/// <summary>
/// <inheritdoc cref="AssetDatabase"/>
/// </summary>
/// <param name="database"></param>
public void SetAssetDatabase(LocalizedAssetDatabase database) => m_AssetDatabase = database;
/// <summary>
/// <inheritdoc cref="AssetDatabase"/>
/// </summary>
/// <returns></returns>
public virtual LocalizedAssetDatabase GetAssetDatabase() => m_AssetDatabase;
/// <summary>
/// Sets the string database to be used for localizing all strings.
/// </summary>
/// <param name="database"></param>
public void SetStringDatabase(LocalizedStringDatabase database) => m_StringDatabase = database;
/// <summary>
/// Returns the string database being used to localize all strings.
/// </summary>
/// <returns>The string database.</returns>
public virtual LocalizedStringDatabase GetStringDatabase() => m_StringDatabase;
/// <summary>
/// Returns the Localization Settings Metadata.
/// Metadata can be used to contain additional information such as App Name localization settings.
/// </summary>
/// <returns></returns>
public MetadataCollection GetMetadata() => m_Metadata;
/// <summary>
/// Sends out notifications when the locale has changed. Ensures the the events are sent in the correct order.
/// </summary>
/// <param name="locale">The new locale.</param>
internal void SendLocaleChangedEvents(Locale locale)
{
#if UNITY_EDITOR
if (locale == null)
{
LocalizationPropertyDriver.UnregisterProperties();
VariantsPropertyDriver.UnregisterProperties();
}
#endif
m_StringDatabase?.OnLocaleChanged(locale);
m_AssetDatabase?.OnLocaleChanged(locale);
if (m_InitializingOperationHandle.IsValid())
{
AddressablesInterface.SafeRelease(m_InitializingOperationHandle);
m_InitializingOperationHandle = default;
}
var initOp = GetInitializationOperation();
if (initOp.Status == AsyncOperationStatus.Succeeded)
{
InvokeSelectedLocaleChanged(locale);
}
else
{
// We use a coroutine to call the OnSelectedLocaleChanged event, we do not want to use the initOp Completed event as this
// will create issues if a user was to call WaitForCompletion inside of the callback, it would fail with:
// "Exception: Reentering the Update method is not allowed. This can happen when calling WaitForCompletion on
// an operation while inside of a callback".
LocalizationBehaviour.Instance.StartCoroutine(InitializeAndCallSelectedLocaleChangedCoroutine(locale));
}
}
IEnumerator InitializeAndCallSelectedLocaleChangedCoroutine(Locale locale)
{
yield return m_InitializingOperationHandle;
InvokeSelectedLocaleChanged(locale);
}
void InvokeSelectedLocaleChanged(Locale locale)
{
IsChangingSelectedLocale = true;
try
{
m_SelectedLocaleChanged.LockForChanges();
var len = m_SelectedLocaleChanged.Length;
if (len == 1)
{
m_SelectedLocaleChanged.SingleDelegate(locale);
}
else if (len > 1)
{
var array = m_SelectedLocaleChanged.MultiDelegates;
for (int i = 0; i < len; ++i)
array[i](locale);
}
}
catch (Exception ex)
{
Debug.LogException(ex);
}
IsChangingSelectedLocale = false;
m_SelectedLocaleChanged.UnlockForChanges();
}
#if UNITY_EDITOR
internal static string EditorLocaleCode
{
get => UnityEditor.EditorPrefs.GetString(ConfigEditorLocale, string.Empty);
set => UnityEditor.EditorPrefs.SetString(ConfigEditorLocale, value);
}
#endif
Locale SelectActiveLocale()
{
if (m_AvailableLocales == null)
{
Debug.LogError("AvailableLocales is null, can not pick a Locale.");
return null;
}
if (m_AvailableLocales.Locales == null)
{
Debug.LogError("AvailableLocales.Locales is null, can not pick a Locale.");
return null;
}
#if UNITY_EDITOR
if (!IsPlayingOrWillChangePlaymode)
{
return m_AvailableLocales.GetLocale(EditorLocaleCode);
}
#endif
return SelectLocaleUsingStartupSelectors();
}
/// <summary>
/// Uses <see cref="StartupLocaleSelectors"/> to select the most appropriate <see cref="Locale"/>.
/// </summary>
protected internal virtual Locale SelectLocaleUsingStartupSelectors()
{
foreach (var sel in m_StartupSelectors)
{
var locale = sel.GetStartupLocale(GetAvailableLocales());
if (locale != null)
{
return locale;
}
}
using (StringBuilderPool.Get(out var sb))
{
sb.AppendLine("No Locale could be selected:");
if (m_AvailableLocales.Locales.Count == 0)
{
sb.AppendLine("No Locales were available. Did you build the Addressables?");
}
else
{
sb.AppendLine($"The following ({m_AvailableLocales.Locales.Count}) Locales were considered:");
foreach (var locale in m_AvailableLocales.Locales)
{
sb.AppendLine($"\t{locale}");
}
}
sb.AppendLine($"The following ({m_StartupSelectors.Count}) IStartupLocaleSelectors were used:");
foreach (var selector in m_StartupSelectors)
{
sb.AppendLine($"\t{selector}");
}
Debug.LogError(sb.ToString(), this);
}
return null;
}
/// <summary>
/// <inheritdoc cref="SelectedLocale"/>
/// </summary>
/// <param name="locale"></param>
public void SetSelectedLocale(Locale locale)
{
// Do nothing if we are assigning the same locale
if (m_SelectedLocaleAsync.IsValid())
{
var previousLocale = m_SelectedLocaleAsync.Result;
if (ReferenceEquals(previousLocale, locale))
return;
// Sometimes users will hold a list of Locales in a script and assign them in the player,
// these assets are now copies and wont have the same reference so we should also check the asset code and name.(LOC-1096)
if (previousLocale != null && locale != null && previousLocale.name == locale.name && previousLocale.Identifier.Code == locale.Identifier.Code)
return;
}
// We need to ensure initialization has been started
GetInitializationOperation();
#if UNITY_EDITOR
// Running the player loop outside of play mode will force an update for many types, especially UGUI.
if (!IsPlayingOrWillChangePlaymode)
{
UnityEditor.EditorApplication.QueuePlayerLoopUpdate();
}
#endif
// Ignore null locales in play mode
if (locale == null && IsPlayingOrWillChangePlaymode)
return;
if (!m_SelectedLocaleAsync.IsValid() || !ReferenceEquals(m_SelectedLocaleAsync.Result, locale))
{
#if UNITY_EDITOR
if (!IsPlayingOrWillChangePlaymode)
{
var code = locale == null ? string.Empty : locale.Identifier.Code;
EditorLocaleCode = code;
}
#endif
if (m_SelectedLocaleAsync.IsValid())
AddressablesInterface.Release(m_SelectedLocaleAsync);
m_SelectedLocaleAsync = AddressablesInterface.ResourceManager.CreateCompletedOperation(locale, null);
SendLocaleChangedEvents(locale);
}
}
/// <summary>
/// <inheritdoc cref="SelectedLocaleAsync"/>
/// </summary>
/// <returns></returns>
public virtual AsyncOperationHandle<Locale> GetSelectedLocaleAsync()
{
if (!m_SelectedLocaleAsync.IsValid())
{
if (m_AvailableLocales is IPreloadRequired localesProvider && !localesProvider.PreloadOperation.IsDone)
{
m_SelectedLocaleAsync = AddressablesInterface.ResourceManager.CreateChainOperation(localesProvider.PreloadOperation, (op) => AddressablesInterface.ResourceManager.CreateCompletedOperation(SelectActiveLocale(), null));
}
else
{
m_SelectedLocaleAsync = AddressablesInterface.ResourceManager.CreateCompletedOperation(SelectActiveLocale(), null);
}
}
return m_SelectedLocaleAsync;
}
/// <summary>
/// <inheritdoc cref="SelectedLocale"/>
/// </summary>
/// <returns>\</returns>
public virtual Locale GetSelectedLocale()
{
var localeOp = GetSelectedLocaleAsync();
if (localeOp.IsDone)
return localeOp.Result;
return localeOp.WaitForCompletion();
}
/// <summary>
/// Indicates that the Locale is no longer available.
/// If the locale is the current <see cref="SelectedLocale"/> then a new one will be found using <see cref="StartupLocaleSelectors"/>.
/// </summary>
/// <param name="locale"></param>
public virtual void OnLocaleRemoved(Locale locale)
{
if (m_SelectedLocaleAsync.IsValid() && ReferenceEquals(m_SelectedLocaleAsync.Result, locale))
{
AddressablesInterface.Release(m_SelectedLocaleAsync);
m_SelectedLocaleAsync = default;
}
}
/// <inheritdoc/>
public void ResetState()
{
m_SelectedLocaleAsync = default;
m_InitializingOperationHandle = default;
(m_AvailableLocales as IReset)?.ResetState();
(m_AssetDatabase as IReset)?.ResetState();
(m_StringDatabase as IReset)?.ResetState();
}
/// <summary>
/// Releases all Addressables assetss.
/// </summary>
void IDisposable.Dispose()
{
if (m_InitializingOperationHandle.IsValid())
{
if (!m_InitializingOperationHandle.IsDone)
m_InitializingOperationHandle.WaitForCompletion();
AddressablesInterface.Release(m_InitializingOperationHandle);
}
if (m_SelectedLocaleAsync.IsValid())
{
Debug.Assert(m_SelectedLocaleAsync.IsDone, "Disposing an incomplete locale operation");
AddressablesInterface.Release(m_SelectedLocaleAsync);
}
m_InitializingOperationHandle = default;
m_SelectedLocaleAsync = default;
(m_AvailableLocales as IDisposable)?.Dispose();
(m_AssetDatabase as IDisposable)?.Dispose();
(m_StringDatabase as IDisposable)?.Dispose();
GC.SuppressFinalize(this);
}
/// <summary>
/// Returns the singleton of the LocalizationSettings but does not create a default one if no active settings are found.
/// </summary>
/// <returns></returns>
public static LocalizationSettings GetInstanceDontCreateDefault()
{
// Use ReferenceEquals so we dont get false positives when using MoQ
if (!ReferenceEquals(s_Instance, null))
return s_Instance;
LocalizationSettings settings;
#if UNITY_EDITOR
UnityEditor.EditorBuildSettings.TryGetConfigObject(ConfigName, out settings);
#else
settings = FindObjectOfType<LocalizationSettings>();
#endif
return settings;
}
static LocalizationSettings GetOrCreateSettings()
{
var settings = GetInstanceDontCreateDefault();
// Use ReferenceEquals so we dont get false positives when using MoQ
if (ReferenceEquals(settings, null))
{
Debug.LogWarning("Could not find localization settings. Default will be used.");
settings = CreateInstance<LocalizationSettings>();
settings.name = "Default Localization Settings";
}
return settings;
}
}
}