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
{
///
/// The localization settings is the core component to the localization system.
/// It provides the entry point to all player based localization features.
///
public class LocalizationSettings : ScriptableObject, IReset, IDisposable
{
///
/// The name to use when retrieving the LocalizationSettings from CustomObject API.
///
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 m_StartupSelectors = new List
{
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 m_InitializingOperationHandle;
AsyncOperationHandle m_SelectedLocaleAsync;
Locale m_ProjectLocale;
CallbackArray> m_SelectedLocaleChanged;
internal bool IsChangingSelectedLocale { get; private set; }
internal static LocalizationSettings s_Instance;
///
/// Called when the is changed.
/// This will be called after is completed so any preloading operations will be finished.
///
///
/// Returns if has any subscribers.
///
internal bool HasSelectedLocaleChangedSubscribers => m_SelectedLocaleChanged.Length != 0;
///
/// Called when the is changed.
/// This will be called after is completed so any preloading operations will be finished.
///
public event Action OnSelectedLocaleChanged
{
add => m_SelectedLocaleChanged.Add(value);
remove => m_SelectedLocaleChanged.RemoveByMovingTail(value);
}
///
/// Indicates if there is a LocalizationSettings present. If one is not found then it will attempt to find one however
/// unlike it will not create a default, if one can not be found.
///
/// if has settings; otherwise, .
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);
}
}
///
/// 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 is 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
/// will be ignored when running on WebGL.
///
///
/// This shows how to use a coroutine to wait for the Initialization Operation to complete.
///
///
///
/// This shows how to use the event to get a callback when the Initialization Operation is complete.
///
///
///
/// 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.
///
///
public static AsyncOperationHandle InitializationOperation => Instance.GetInitializationOperation();
///
/// Singleton instance for the Localization Settings.
///
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;
}
///
///
///
public static List StartupLocaleSelectors => Instance.GetStartupLocaleSelectors();
///
///
///
public static ILocalesProvider AvailableLocales
{
get => Instance.GetAvailableLocales();
set => Instance.SetAvailableLocales(value);
}
///
/// The asset database is responsible for providing localized assets.
///
public static LocalizedAssetDatabase AssetDatabase
{
get => Instance.GetAssetDatabase();
set => Instance.SetAssetDatabase(value);
}
///
/// The string database is responsible for providing localized string assets.
///
public static LocalizedStringDatabase StringDatabase
{
get => Instance.GetStringDatabase();
set => Instance.SetStringDatabase(value);
}
///
/// Returns the Localization Settings Metadata.
/// Metadata can be used to contain additional information such as App Name localization settings.
///
public static MetadataCollection Metadata => Instance.GetMetadata();
///
/// The current selected . 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 for a version that will load the Locales asynchronously.
///
public static Locale SelectedLocale
{
get => Instance.GetSelectedLocale();
set => Instance.SetSelectedLocale(value);
}
///
/// The current selected Locale. This is the Locale that will be used by default when localizing assets and strings.
/// If has not been completed yet then this will wait for the part to complete first.
/// It will not wait for the entire but just the part that initializes the Locales.
/// See for a synchronous version that will block until the Locales have been loaded.
///
public static AsyncOperationHandle SelectedLocaleAsync => Instance.GetSelectedLocaleAsync();
///
/// Event that is sent when the is changed.
///
///
/// This shows how to keep track of the current selected .
///
///
public static event Action SelectedLocaleChanged
{
add => Instance.OnSelectedLocaleChanged += value;
remove => Instance.OnSelectedLocaleChanged -= value;
}
///
/// 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.
///
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;
}
}
///
/// Forces the 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
/// will be ignored when running on WebGL.
///
public static bool InitializeSynchronously
{
get => Instance.m_InitializeSynchronously;
set => Instance.m_InitializeSynchronously = value;
}
///
/// Determines which tables, that have been marked as preload, will be loaded during the preloading step.
///
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
///
/// Used to ensure a exists in the project.
/// Throws an exception if is false.
///
///
internal static void ValidateSettingsExist(string error = "")
{
if (!HasSettings)
{
throw new Exception($"There is no active LocalizationSettings.\n {error}");
}
}
///
///
///
///
public virtual AsyncOperationHandle 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
///
/// We use this for testing so we don't have to enter play mode.
///
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;
///
///
///
/// \
public List GetStartupLocaleSelectors() => m_StartupSelectors;
///
///
///
///
public void SetAvailableLocales(ILocalesProvider available) => m_AvailableLocales = available;
///
///
///
/// \
public virtual ILocalesProvider GetAvailableLocales() => m_AvailableLocales;
///
///
///
///
public void SetAssetDatabase(LocalizedAssetDatabase database) => m_AssetDatabase = database;
///
///
///
///
public virtual LocalizedAssetDatabase GetAssetDatabase() => m_AssetDatabase;
///
/// Sets the string database to be used for localizing all strings.
///
///
public void SetStringDatabase(LocalizedStringDatabase database) => m_StringDatabase = database;
///
/// Returns the string database being used to localize all strings.
///
/// The string database.
public virtual LocalizedStringDatabase GetStringDatabase() => m_StringDatabase;
///
/// Returns the Localization Settings Metadata.
/// Metadata can be used to contain additional information such as App Name localization settings.
///
///
public MetadataCollection GetMetadata() => m_Metadata;
///
/// Sends out notifications when the locale has changed. Ensures the the events are sent in the correct order.
///
/// The new locale.
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();
}
///
/// Uses to select the most appropriate .
///
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;
}
///
///
///
///
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);
}
}
///
///
///
///
public virtual AsyncOperationHandle 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;
}
///
///
///
/// \
public virtual Locale GetSelectedLocale()
{
var localeOp = GetSelectedLocaleAsync();
if (localeOp.IsDone)
return localeOp.Result;
return localeOp.WaitForCompletion();
}
///
/// Indicates that the Locale is no longer available.
/// If the locale is the current then a new one will be found using .
///
///
public virtual void OnLocaleRemoved(Locale locale)
{
if (m_SelectedLocaleAsync.IsValid() && ReferenceEquals(m_SelectedLocaleAsync.Result, locale))
{
AddressablesInterface.Release(m_SelectedLocaleAsync);
m_SelectedLocaleAsync = default;
}
}
///
public void ResetState()
{
m_SelectedLocaleAsync = default;
m_InitializingOperationHandle = default;
(m_AvailableLocales as IReset)?.ResetState();
(m_AssetDatabase as IReset)?.ResetState();
(m_StringDatabase as IReset)?.ResetState();
}
///
/// Releases all Addressables assetss.
///
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);
}
///
/// Returns the singleton of the LocalizationSettings but does not create a default one if no active settings are found.
///
///
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();
#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();
settings.name = "Default Localization Settings";
}
return settings;
}
}
}