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