/* Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. */ using System.Collections.Generic; using UnityEngine; using System; #if UNITY_EDITOR using UnityEditor; using System.Linq; #endif #nullable enable namespace Yarn.Unity { [CreateAssetMenu(fileName = "NewLocalization", menuName = "Yarn Spinner/Built-In Localization/Localization", order = 105)] public class Localization : ScriptableObject { /// /// Returns the address that should be used to fetch an asset suitable /// for a specific line in a specific language. /// /// /// This method is useful for creating an address for use with the /// Addressable Assets system. /// /// The line ID to use when generating the /// address. /// The language to use when generating the /// address. /// The address to use. internal static string GetAddressForLine(string lineID, string language) { return $"line_{language}_{lineID.Replace("line:", "")}"; } [System.Serializable] public sealed class LocalizationTableEntry { public string? localizedString; public UnityEngine.Object? localizedAsset; #if USE_ADDRESSABLES public UnityEngine.AddressableAssets.AssetReference? localizedAssetReference; #endif } [SerializeField] internal SerializableDictionary entries = new(); private Dictionary _runtimeStringTable = new Dictionary(); /// /// Gets a value indicating whether this /// makes use of Addressable Assets (), or if it /// stores its assets as direct references (). /// /// /// If this property is , and should not be used to retrieve /// localised objects. Instead, the Addressable Assets API should be /// used. /// public bool UsesAddressableAssets { get => _usesAddressableAssets; internal set => _usesAddressableAssets = value; } [SerializeField] private bool _containsLocalizedAssets; [SerializeField] internal bool _usesAddressableAssets; #region Localized Strings public string? GetLocalizedString(string key) { if (_runtimeStringTable.TryGetValue(key, out string result)) { return result; } if (entries.TryGetValue(key, out var entry)) { return entry.localizedString; } return null; } /// /// Returns a boolean value indicating whether this contains a string with the given key. /// /// The key to search for. /// if this Localization has a string /// for the given key; otherwise. public bool ContainsLocalizedString(string key) => _runtimeStringTable.ContainsKey(key) || entries.ContainsKey(key); #if UNITY_EDITOR /// /// Adds a new string to the string table. /// /// /// This method updates the localisation asset on disk. It is not /// recommended to call this method during play mode, because changes /// will persist after you leave and may cause conflicts. /// This method is only available in the Editor. /// /// The key for this string (generally, the line /// ID.) /// The user-facing text for this string, in the /// language specified by . internal void AddLocalisedStringToAsset(string key, string value) { GetOrCreateEntry(key).localizedString = value; } #endif /// /// Adds a new string to the runtime string table. /// /// /// This method updates the localisation's runtime string table, which /// is useful for adding or changing the localisation during gameplay or /// in a built player. It doesn't modify the asset on disk, and any /// changes made will be lost when gameplay ends. /// /// The key for this string (generally, the line /// ID.) /// The user-facing text for this string, in the /// language specified by . public void AddLocalizedString(string key, string value) { _runtimeStringTable.Add(key, value); } /// /// Adds a collection of strings to the runtime string table. /// /// /// The collection of keys and strings to /// add. public void AddLocalizedStrings(IEnumerable> strings) { foreach (var entry in strings) { AddLocalizedString(entry.Key, entry.Value); } } /// /// Adds a collection of strings to the runtime string table. /// /// /// The collection of objects to add. public void AddLocalizedStrings(IEnumerable stringTableEntries) { foreach (var entry in stringTableEntries) { if (entry.Text != null) { AddLocalizedString(entry.ID, entry.Text); } } } #endregion #region Localised Objects #if USE_ADDRESSABLES public async YarnTask GetLocalizedObjectAsync(string key) where T : UnityEngine.Object { if (!entries.TryGetValue(key, out var entry)) { return null; } if (_usesAddressableAssets) { if (entry.localizedAssetReference == null || entry.localizedAssetReference.RuntimeKeyIsValid() == false) { return null; } // Try to fetch the referenced asset return await UnityEngine.AddressableAssets.Addressables.LoadAssetAsync(entry.localizedAssetReference).Task; } if (entry.localizedAsset is T resultAsTargetObject) { return resultAsTargetObject; } return null; } #else public YarnTask GetLocalizedObjectAsync(string key) where T : UnityEngine.Object { if (!entries.TryGetValue(key, out var entry)) { return YarnTask.FromResult(null); } if (entry.localizedAsset is T resultAsTargetObject) { return YarnTask.FromResult(resultAsTargetObject); } return YarnTask.FromResult(null); } #endif #if UNITY_EDITOR internal T? GetLocalizedObjectSync(string key) where T : UnityEngine.Object { if (!entries.TryGetValue(key, out var entry)) { return null; } #if USE_ADDRESSABLES if (_usesAddressableAssets) { if (entry.localizedAssetReference == null || entry.localizedAssetReference.RuntimeKeyIsValid() == false) { return null; } // Try to fetch the referenced asset return entry.localizedAssetReference.editorAsset as T; } #endif if (entry.localizedAsset is T resultAsTargetObject) { return resultAsTargetObject; } return null; } #endif private LocalizationTableEntry GetOrCreateEntry(string key) { if (entries.TryGetValue(key, out var entry)) { return entry; } entry = new LocalizationTableEntry(); entries.Add(key, entry); return entry; } public bool ContainsLocalizedObject(string key) where T : UnityEngine.Object => entries.TryGetValue(key, out var asset) && asset is T; #if UNITY_EDITOR public void AddLocalizedObjectToAsset(string key, T value) where T : UnityEngine.Object { var entry = GetOrCreateEntry(key); #if USE_ADDRESSABLES if (this.UsesAddressableAssets) { // This Localization uses Addressables, so rather than storing a // direct reference to the asset, we'll use an indirect // AssetReference. entry.localizedAssetReference = new UnityEngine.AddressableAssets.AssetReference(); entry.localizedAssetReference.SetEditorAsset(value); entry.localizedAsset = null; return; } else { // Addressables are available, but we're not using addressable // assets, so clear out any asset references. entry.localizedAssetReference = null; } #endif entry.localizedAsset = value; } #endif #endregion public virtual void Clear() { entries.Clear(); _runtimeStringTable.Clear(); } /// /// Gets the line IDs present in this localization. /// /// /// The line IDs can be used to access the localized text or asset /// associated with a line. /// /// The line IDs. public IEnumerable GetLineIDs() { var allKeys = new List(); var runtimeKeys = _runtimeStringTable.Keys; var compileTimeKeys = entries.Keys; allKeys.AddRange(runtimeKeys); allKeys.AddRange(compileTimeKeys); return allKeys; } } } #if UNITY_EDITOR namespace Yarn.Unity { /// /// Provides methods for finding voice over s in the /// project matching a Yarn linetag/string ID and a language ID. /// public static class FindVoiceOver { /// /// Finds all voice over s in the project with a /// filename matching a Yarn linetag and a language ID. /// /// The linetag/string ID the voice over filename /// should match. /// The language ID the voice over filename /// should match. /// A string array with GUIDs of all matching s. public static string[] GetMatchingVoiceOverAudioClip(string linetag, string language) { var lineTagContents = linetag.Replace("line:", ""); string[] result = Array.Empty(); string[] searchPatterns = new string[] { $"t:AudioClip {lineTagContents} ({language})", $"t:AudioClip {lineTagContents} {language}", $"t:AudioClip {lineTagContents}" }; foreach (var searchPattern in searchPatterns) { result = SearchAssetDatabase(searchPattern, language); if (result.Length > 0) { return result; } } return result; } public static string[] SearchAssetDatabase(string searchPattern, string language) { var result = AssetDatabase.FindAssets(searchPattern); // Check if result is ambiguous and try to improve the situation if (result.Length > 1) { var assetsInMatchingLanguageDirectory = GetAsseetsInMatchingLanguageDirectory(result, language); // Check if this improved the situation if (assetsInMatchingLanguageDirectory.Length == 1 || (assetsInMatchingLanguageDirectory.Length != 0 && assetsInMatchingLanguageDirectory.Length < result.Length)) { result = assetsInMatchingLanguageDirectory; } } return result; } public static string[] GetAsseetsInMatchingLanguageDirectory(string[] result, string language) { var list = new List(); foreach (var assetId in result) { var testPath = AssetDatabase.GUIDToAssetPath(assetId); if (AssetDatabase.GUIDToAssetPath(assetId).Contains($"/{language}/")) { list.Add(assetId); } } return list.ToArray(); } } } #endif