using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine.Localization.Settings;
using UnityEngine.Localization.SmartFormat.Core.Extensions;
using UnityEngine.Localization.SmartFormat.Core.Formatting;
using UnityEngine.Localization.SmartFormat.Extensions;
using UnityEngine.Localization.SmartFormat.PersistentVariables;
using UnityEngine.Localization.Tables;
using UnityEngine.Pool;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace UnityEngine.Localization
{
///
/// Provides a way to reference a inside of a specific and request the localized string.
///
///
/// This example shows how to localize a [MonoBehaviour](https://docs.unity3d.com/ScriptReference/MonoBehaviour.html) so that it updates automatically when the active locale changes.
/// This example uses asynchronous loading to load the localized assets in the background.
///
///
///
/// This example shows how to localize a [MonoBehaviour](https://docs.unity3d.com/ScriptReference/MonoBehaviour.html) immediately.
/// This example uses synchronous loading, which may cause a pause when first loading the localized assets.
///
///
///
/// This example shows how to localize a [ScriptableObject](https://docs.unity3d.com/ScriptReference/ScriptableObject.html) to represent a quest in a game.
///
///
///
/// This example shows how to use local variables to represent a health counter.
///
///
///
/// This example shows how to bind to a UI Toolkit property. Note that this requires Unity 2023.2 and above.
///
///
///
/// This example shows how to use local variables when you bind to a UI Toolkit property. Note that this requires Unity 2023.2 and above.
///
///
[Serializable]
public partial class LocalizedString : LocalizedReference, IVariableGroup, IDictionary, IVariableValueChanged, IDisposable
{
[SerializeField]
List m_LocalVariables = new List();
CallbackArray m_ChangeHandler;
string m_CurrentStringChangedValue;
// Kept in sync with m_Variables so that users can make changes via the inspector without issues(duplicate/empty names etc).
readonly Dictionary m_VariableLookup = new Dictionary();
readonly List m_UsedVariables = new List();
Action m_OnVariableChanged;
Action m_SelectedLocaleChanged;
Action> m_AutomaticLoadingCompleted;
Action.TableEntryResult>> m_CompletedSourceValue;
bool m_WaitingForVariablesEndUpdate;
///
public event Action ValueChanged;
internal override bool ForceSynchronous => WaitForCompletion || LocalizationSettings.StringDatabase.AsynchronousBehaviour == AsynchronousBehaviour.ForceSynchronous;
///
/// Arguments that will be passed through to Smart Format. These arguments are not serialized and will need to be set at runtime.
/// See to add persistent serialized arguments.
///
public IList Arguments { get; set; }
///
/// The current loading operation for the string when using or if one is not available.
/// A string may not be immediately available, such as when loading the asset, so all string operations are wrapped
/// with an .
/// See also
///
public AsyncOperationHandle CurrentLoadingOperationHandle
{
get;
internal set;
}
///
/// Delegate used by .
///
/// The localized string.
public delegate void ChangeHandler(string value);
///
/// Provides a callback that will be invoked when the translated string has changed.
/// The following events will trigger an update:
///
/// The first time the action is added to the event.
/// The changing.
/// If the string is currently using a which supports and it's value has changed.
/// When is called.
/// The or changing.
///
/// When the first is added, a loading operation (see ) automatically starts.
/// When the loading operation is completed, the localized string value is sent to the subscriber.
/// If you add additional subscribers after loading has completed, they are also sent the latest localized string value.
/// This ensures that a subscriber will always have the correct localized value regardless of when it was added.
///
///
/// This example shows how the event can be used to trigger updates to a string.
///
///
public event ChangeHandler StringChanged
{
add
{
if (value == null)
throw new ArgumentNullException();
m_ChangeHandler.Add(value);
if (m_ChangeHandler.Length == 1)
{
LocalizationSettings.ValidateSettingsExist();
ForceUpdate();
// We subscribe after the first update as its possible that a SelectedLocaleChanged may be fired
// during ForceUpdate when using WaitForCompletion and we want to avoid this.
LocalizationSettings.SelectedLocaleChanged += m_SelectedLocaleChanged;
}
else if (CurrentLoadingOperationHandle.IsValid() && CurrentLoadingOperationHandle.IsDone)
{
// Call the event with the latest value.
value(m_CurrentStringChangedValue);
}
}
remove
{
m_ChangeHandler.RemoveByMovingTail(value);
if (m_ChangeHandler.Length == 0)
{
LocalizationSettings.SelectedLocaleChanged -= m_SelectedLocaleChanged;
ClearLoadingOperation();
}
}
}
///
/// Returns if has any subscribers.
///
public bool HasChangeHandler => m_ChangeHandler.Length != 0;
///
/// Initializes and returns an empty instance of a .
///
public LocalizedString()
{
m_SelectedLocaleChanged = HandleLocaleChange;
m_OnVariableChanged = OnVariableChanged;
m_AutomaticLoadingCompleted = AutomaticLoadingCompleted;
m_CompletedSourceValue = CompletedSourceValue;
}
///
/// Initializes and returns an instance of a .
///
/// Reference to the String Table Collection.
/// This can either be the name of the collection as a or the Collection Guid as a [System.Guid](https://docs.microsoft.com/en-us/dotnet/api/system.guid).
/// Reference to the String Table Collection entry.
/// This can either be the name of the Key as a or the Key Id.
///
/// This example shows the different ways to construct a LocalizedString.
///
///
///
/// This example shows how a LocalizedString could be set up in the Editor.
/// By using the Guid and Id the table and entry references will not be lost if the table collection name or entry name was to be changed.
/// Note: The performance benefits to using a Guid and Id are negligible.
///
///
public LocalizedString(TableReference tableReference, TableEntryReference entryReference) : this()
{
TableReference = tableReference;
TableEntryReference = entryReference;
}
///
/// Provides a way to force a refresh of the string when using .
///
///
/// This will only force the refresh if there is currently no active , if one is still being executed then it will be ignored and will be returned.
/// If a string is not static and will change during game play, such as when using format arguments, then this can be used to force the string to update itself.
/// You may wish to call this if the values inside of the list have changed or you wish to force all subscribers to update.
///
/// Returns if a new refresh could be requested or if it could not, such as when is still loading.
///
/// This example shows how the string can be refreshed, such as when showing dynamic values like the current time.
///
///
public bool RefreshString()
{
if (m_ChangeHandler.Length == 0 || !CurrentLoadingOperationHandle.IsValid())
return false;
if (!CurrentLoadingOperationHandle.IsDone)
{
#if !UNITY_WEBGL
if (ForceSynchronous)
{
CurrentLoadingOperationHandle.WaitForCompletion();
return true;
}
else
#endif
return false;
}
var entry = CurrentLoadingOperationHandle.Result.Entry;
var formatCache = entry?.GetOrCreateFormatCache();
if (formatCache != null)
{
formatCache.LocalVariables = this;
formatCache.VariableTriggers.Clear();
}
var translatedText = LocalizationSettings.StringDatabase.GenerateLocalizedString(CurrentLoadingOperationHandle.Result.Table, entry, TableReference, TableEntryReference, LocalizationSettings.SelectedLocale, Arguments);
if (formatCache != null)
{
formatCache.LocalVariables = null;
UpdateVariableListeners(entry?.FormatCache?.VariableTriggers);
}
m_CurrentStringChangedValue = translatedText;
InvokeChangeHandler(m_CurrentStringChangedValue);
return true;
}
///
/// Provides a translated string from a with the and
/// the translated string that matches .
///
///
/// The event provides a notification once the operation has finished and the string has been found or an error has occurred.
/// A string table may have already been loaded during a previous operation or when using Preload mode.
/// Check the property to see if the string table has already been loaded and the translated string is immediately available.
/// See [Async operation handling](https://docs.unity3d.com/Packages/com.unity.addressables@latest/index.html?subfolder=/manual/AddressableAssetsAsyncOperationHandle.html) for further details.
/// To force the operation to complete immediately, call .
///
/// Returns the loading operation for the request.
///
/// This example shows how can be used to request an updated string when the changes.
///
///
///
/// This example shows how can be forced to complete immediately using .
///
///
public AsyncOperationHandle GetLocalizedStringAsync() => GetLocalizedStringAsync(Arguments);
///
/// Provides a translated string from a with the and
/// the translated string that matches .
/// 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).
///
/// The localized string for the or if it is not .
public string GetLocalizedString() => GetLocalizedStringAsync().WaitForCompletion();
///
/// Provides a translated string from a with the and
/// the translated string that matches .
///
/// The arguments to pass into the Smart String formatter or [String.Format](https://docs.microsoft.com/en-us/dotnet/api/system.string.format).
/// Returns the loading operation for the request.
public AsyncOperationHandle GetLocalizedStringAsync(params object[] arguments) => GetLocalizedStringAsync((IList)arguments);
///
/// Provides a translated string from a with the and
/// the translated string that matches .
/// 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).
///
/// The arguments to pass into the Smart String formatter or [String.Format](https://docs.microsoft.com/en-us/dotnet/api/system.string.format).
/// The localized string for the or if it is not .
public string GetLocalizedString(params object[] arguments) => GetLocalizedStringAsync((IList)arguments).WaitForCompletion();
///
/// Provides a translated string from a with the and
/// the translated string that matches .
///
/// The arguments to pass into the Smart String formatter or [String.Format](https://docs.microsoft.com/en-us/dotnet/api/system.string.format).
/// The localized string for the or if it is not .
public string GetLocalizedString(IList arguments) => GetLocalizedStringAsync(arguments).WaitForCompletion();
///
/// Provides a translated string from a with the and
/// the translated string that matches .
///
/// The arguments to pass into the Smart String formatter or [String.Format](https://docs.microsoft.com/en-us/dotnet/api/system.string.format).
/// Returns the loading operation for the request.
public AsyncOperationHandle GetLocalizedStringAsync(IList arguments)
{
LocalizationSettings.ValidateSettingsExist();
return LocalizationSettings.StringDatabase.GetLocalizedStringAsync(TableReference, TableEntryReference, arguments, LocaleOverride, FallbackState, m_LocalVariables.Count > 0 ? this : null);
}
///
/// Returns the number of local variables inside this localized string.
///
public int Count => m_VariableLookup.Count;
///
/// Returns a collection containing all the unique local variable names.
///
public ICollection Keys => m_VariableLookup.Keys;
///
/// Returns all the local variables for this localized string.
///
public ICollection Values => m_VariableLookup.Values.Select(s => s.variable).ToList();
///
/// Implemented as part of the IDictionary interface but not actually used. Will always return .
///
public bool IsReadOnly => false;
///
/// Gets or sets the with the specified name.
///
/// The name of the variable.
/// The found variable.
/// Thrown if a variable with the specified name does not exist.
///
/// This example shows how to get and add a local variable.
///
///
public IVariable this[string name]
{
get => m_VariableLookup[name].variable;
set => Add(name, value);
}
///
/// Gets the with the specified name.
///
/// The name of the variable.
/// The variable that was found or .
/// if a variable was found and if one could not.
///
/// This example shows how to get and add a local variable using TryGetValue.
///
///
public bool TryGetValue(string name, out IVariable value)
{
if (m_VariableLookup.TryGetValue(name, out var v))
{
value = v.variable;
return true;
}
value = default;
return false;
}
///
/// Adds a new Local Variable to use during formatting.
///
/// The name of the variable, must be unique. Note the name should not contain any whitespace, if any is found then it will be replaced with with '-'.
/// The variable to use when formatting. See also , , , , .
/// Thrown when is null or empty.
/// Thrown when variable is null.
///
/// This example shows how to get and add a local variable.
///
///
public void Add(string name, IVariable variable)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException(nameof(name), "Name must not be null or empty.");
if (variable == null)
throw new ArgumentNullException(nameof(variable));
name = name.ReplaceWhiteSpaces("-");
if (m_VariableLookup.TryGetValue(name, out var value))
{
if (ReferenceEquals(value.variable, variable))
return;
m_LocalVariables.Remove(value);
}
var v = new VariableNameValuePair { name = name, variable = variable };
m_VariableLookup[name] = v;
m_LocalVariables.Add(v);
}
///
///
///
/// The local variable name and value to add.
public void Add(KeyValuePair item) => Add(item.Key, item.Value);
///
/// Removes a local variable with the specified name.
///
/// The name of the variable to be removed.
/// if a variable with the specified name was removed, if one was not.
///
/// This example shows how to remove a local variable.
///
///
public bool Remove(string name)
{
if (m_VariableLookup.TryGetValue(name, out var v))
{
m_LocalVariables.Remove(v);
m_VariableLookup.Remove(name);
return true;
}
return false;
}
///
/// Removes a local variable with the specified key.
///
/// The item to be removed, only the Key field will be considered.
/// if a variable with the specified name was removed, if one was not.
public bool Remove(KeyValuePair item) => Remove(item.Key);
///
/// Returns if a local variable with the specified name exists.
///
/// The variable name to check for.
/// if a matching variable could be found or if one could not.
public bool ContainsKey(string name) => m_VariableLookup.ContainsKey(name);
///
///
///
/// The item to check for. Both the Key and Value must match.
/// if a matching variable could be found or if one could not.
public bool Contains(KeyValuePair item) => TryGetValue(item.Key, out var v) && v == item.Value;
///
/// Copies the local variables into an array starting at .
///
/// The array to copy the local variables into.
/// The index to start copying the items into.
/// Thrown when the array is null.
public void CopyTo(KeyValuePair[] array, int arrayIndex)
{
if (array == null)
throw new ArgumentNullException(nameof(array));
foreach (var entry in m_VariableLookup)
{
array[arrayIndex++] = new KeyValuePair(entry.Key, entry.Value.variable);
}
}
///
///
///
/// The enumerator that can be used to iterate through all the local variables.
IEnumerator> IEnumerable>.GetEnumerator()
{
foreach (var v in m_VariableLookup)
{
yield return new KeyValuePair(v.Key, v.Value.variable);
}
}
///
/// Returns an enumerator for all local variables in this localized string.
///
/// The enumerator that can be used to iterate through all the local variables.
public IEnumerator GetEnumerator()
{
foreach (var v in m_VariableLookup)
{
yield return new KeyValuePair(v.Key, v.Value.variable);
}
}
///
/// Removes all local variables.
///
public void Clear()
{
m_VariableLookup.Clear();
m_LocalVariables.Clear();
}
///
/// Allows for accessing metadata in a smart string.
///
struct StringTableEntryVariable : IVariableGroup
{
readonly string m_Localized;
readonly StringTableEntry m_StringTableEntry;
public StringTableEntryVariable(string localized, StringTableEntry entry)
{
m_Localized = localized;
m_StringTableEntry = entry;
}
public bool TryGetValue(string key, out IVariable value)
{
foreach (var md in m_StringTableEntry.MetadataEntries)
{
if (md is IMetadataVariable v && v.VariableName == key)
{
value = v;
return true;
}
}
value = null;
return false;
}
///
/// Returns the localized string by default.
///
/// The localized string.
public override string ToString() => m_Localized;
}
///
/// Provides access to both the current local variables and those from the parent.
///
struct ChainedLocalVariablesGroup : IVariableGroup
{
IVariableGroup ParentGroup { get; set; }
IVariableGroup Group { get; set; }
public ChainedLocalVariablesGroup(IVariableGroup group, IVariableGroup parent)
{
Group = group;
ParentGroup = parent;
}
public bool TryGetValue(string key, out IVariable value)
{
if (Group.TryGetValue(key, out value))
return true;
if (ParentGroup.TryGetValue(key, out value))
return true;
value = null;
return false;
}
}
///
/// Thrown when , no locale is available, the table is still loading, or the entry is missing.
public object GetSourceValue(ISelectorInfo selector)
{
if (IsEmpty)
throw new DataNotReadyException("{Empty}");
// Determine what Locale we should use.
var locale = LocaleOverride;
if (locale == null && selector.FormatDetails.FormatCache != null)
locale = LocalizationSettings.AvailableLocales.GetLocale(selector.FormatDetails.FormatCache.Table.LocaleIdentifier);
if (locale == null && LocalizationSettings.SelectedLocaleAsync.IsDone)
locale = LocalizationSettings.SelectedLocaleAsync.Result;
if (locale == null)
throw new DataNotReadyException("{No Available Locale}");
var operation = LocalizationSettings.StringDatabase.GetTableEntryAsync(TableReference, TableEntryReference, locale, FallbackState);
if (!operation.IsDone)
{
operation.Completed += m_CompletedSourceValue;
throw new DataNotReadyException();
}
var entry = operation.Result.Entry;
if (entry == null)
throw new DataNotReadyException("{Missing Entry}");
// If the entry is not smart then we do not need to forward as much information to the child.
if (!entry.IsSmart)
{
var result = LocalizationSettings.StringDatabase.GenerateLocalizedString(operation.Result.Table, entry, TableReference, TableEntryReference, locale, Arguments);
return new StringTableEntryVariable(result, entry);
}
var formatCache = entry?.GetOrCreateFormatCache();
if (formatCache != null)
{
formatCache.VariableTriggers.Clear();
if (m_VariableLookup.Count > 0)
{
// Use the child and parent local variables.
formatCache.LocalVariables = new ChainedLocalVariablesGroup(this, selector.FormatDetails.FormatCache.LocalVariables);
}
else
{
// Just use the parents local variables.
formatCache.LocalVariables = selector.FormatDetails.FormatCache.LocalVariables;
}
}
using (ListPool.Get(out var args))
{
if (selector.CurrentValue != null)
args.Add(selector.CurrentValue);
if (Arguments != null)
args.AddRange(Arguments);
var result = LocalizationSettings.StringDatabase.GenerateLocalizedString(operation.Result.Table, entry, TableReference, TableEntryReference, locale, args);
if (formatCache != null)
{
formatCache.LocalVariables = null;
// Subscribe to changes to local variables.
// Note: This could cause issues if the nested string is being used in multiple places.
// We may need to consider keeping multiple lists of callbacks in the future.
UpdateVariableListeners(formatCache.VariableTriggers);
}
return new StringTableEntryVariable(result, entry);
}
}
void CompletedSourceValue(AsyncOperationHandle.TableEntryResult> _) => ValueChanged?.Invoke(this);
///
protected internal override void ForceUpdate()
{
if (m_ChangeHandler.Length != 0)
{
HandleLocaleChange(null);
}
ValueChanged?.Invoke(this);
}
void UpdateVariableListeners(List variables)
{
// Unsubscribe from any old ones
foreach (var gv in m_UsedVariables)
{
gv.ValueChanged -= m_OnVariableChanged;
}
m_UsedVariables.Clear();
if (variables == null)
return;
foreach (var gv in variables)
{
m_UsedVariables.Add(gv);
gv.ValueChanged += m_OnVariableChanged;
}
}
void OnVariableChanged(IVariable globalVariable)
{
if (m_WaitingForVariablesEndUpdate)
return;
if (PersistentVariablesSource.IsUpdating)
{
// Its possible that multiple global variables will be changed, we don't want to force the
// string to be updated for each change so we defer and do a single update during EndUpdate.
m_WaitingForVariablesEndUpdate = true;
PersistentVariablesSource.EndUpdate += OnVariablesSourceUpdateCompleted;
}
else
{
RefreshString();
ValueChanged?.Invoke(this);
}
}
void OnVariablesSourceUpdateCompleted()
{
PersistentVariablesSource.EndUpdate -= OnVariablesSourceUpdateCompleted;
m_WaitingForVariablesEndUpdate = false;
RefreshString();
ValueChanged?.Invoke(this);
}
void InvokeChangeHandler(string value)
{
try
{
m_ChangeHandler.LockForChanges();
var len = m_ChangeHandler.Length;
if (len == 1)
{
m_ChangeHandler.SingleDelegate(value);
}
else if (len > 1)
{
var array = m_ChangeHandler.MultiDelegates;
for (int i = 0; i < len; ++i)
array[i](value);
}
}
catch (Exception ex)
{
Debug.LogException(ex);
}
m_ChangeHandler.UnlockForChanges();
}
void HandleLocaleChange(Locale locale)
{
// Cancel any previous loading operations.
ClearLoadingOperation();
m_CurrentStringChangedValue = null;
#if MODULE_UITK && UNITY_2023_3_OR_NEWER && UNITY_EDITOR
HandleLocaleChangeDataBinding(locale);
#endif
#if UNITY_EDITOR
m_CurrentTable = TableReference;
m_CurrentTableEntry = TableEntryReference;
// Don't update if we have no selected Locale
if (!LocalizationSettings.Instance.IsPlayingOrWillChangePlaymode && LocaleOverride == null && LocalizationSettings.SelectedLocale == null)
return;
#endif
if (IsEmpty)
{
#if UNITY_EDITOR
// If we are empty and playing or previewing then we should force an update.
if (!LocalizationSettings.Instance.IsPlayingOrWillChangePlaymode)
InvokeChangeHandler(null);
#endif
return;
}
CurrentLoadingOperationHandle = LocalizationSettings.StringDatabase.GetTableEntryAsync(TableReference, TableEntryReference, LocaleOverride, FallbackState);
AddressablesInterface.Acquire(CurrentLoadingOperationHandle);
if (!CurrentLoadingOperationHandle.IsDone)
{
#if !UNITY_WEBGL
if (ForceSynchronous)
{
CurrentLoadingOperationHandle.WaitForCompletion();
}
else
#endif
{
CurrentLoadingOperationHandle.Completed += m_AutomaticLoadingCompleted;
return;
}
}
AutomaticLoadingCompleted(CurrentLoadingOperationHandle);
}
void AutomaticLoadingCompleted(AsyncOperationHandle loadOperation)
{
if (loadOperation.Status != AsyncOperationStatus.Succeeded)
{
CurrentLoadingOperationHandle = default;
return;
}
RefreshString();
}
void ClearLoadingOperation()
{
if (CurrentLoadingOperationHandle.IsValid())
{
// We should only call this if we are not done as its possible that the internal list is null if its not been used.
if (!CurrentLoadingOperationHandle.IsDone)
CurrentLoadingOperationHandle.Completed -= m_AutomaticLoadingCompleted;
AddressablesInterface.Release(CurrentLoadingOperationHandle);
CurrentLoadingOperationHandle = default;
}
}
///
/// Clears the current loading operation.
///
protected override void Reset() => ClearLoadingOperation();
public override void OnAfterDeserialize()
{
m_VariableLookup.Clear();
foreach (var v in m_LocalVariables)
{
if (!string.IsNullOrEmpty(v.name))
{
m_VariableLookup[v.name] = v;
}
}
}
///
/// Removes and releases internal references to Addressable assets.
///
~LocalizedString()
{
ClearLoadingOperation();
}
///
/// Removes and releases internal references to Addressable assets.
///
void IDisposable.Dispose()
{
m_ChangeHandler.Clear();
ClearLoadingOperation();
LocalizationSettings.SelectedLocaleChanged -= m_SelectedLocaleChanged;
GC.SuppressFinalize(this);
}
}
}