/* Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. */ #if USE_UNITY_LOCALIZATION using System.Collections.Generic; using System.Threading; using UnityEngine; using UnityEngine.Localization; using UnityEngine.Localization.Metadata; using UnityEngine.Localization.Settings; using UnityEngine.Localization.Tables; using UnityEngine.ResourceManagement.Exceptions; using Yarn.Markup; #nullable enable namespace Yarn.Unity.UnityLocalization { /// /// Contains Yarn Spinner related metadata for Unity string table entries. /// [System.Serializable] public class LineMetadata : IMetadata { /// /// The name of the Yarn node that this line came from. /// public string nodeName = ""; /// /// The #hashtags present on the line. /// public string[] tags = System.Array.Empty(); /// /// Gets the line ID indicated by any shadow tag contained in this /// metadata, if present. /// public string? ShadowLineSource { get { foreach (var metadataEntry in tags) { if (metadataEntry.StartsWith("shadow:")) { // This is a shadow line. Return the line ID that it's // shadowing. return "line:" + metadataEntry.Substring("shadow:".Length); } } // The line had metadata, but it wasn't a shadow line. return null; } } } /// /// A line provider that uses the Unity Localization system to get localized /// content for Yarn lines. /// public partial class UnityLocalisedLineProvider : LineProviderBehaviour { // the string table asset that has all of our (hopefully) localised // strings inside [SerializeField] internal LocalizedStringTable? stringsTable; [SerializeField] internal LocalizedAssetTable? assetTable; /// public override string LocaleCode { get => LocalizationSettings.SelectedLocale.Identifier.Code; set { Locale? locale = LocalizationSettings.AvailableLocales.GetLocale(value); if (locale == null) { throw new System.InvalidOperationException($"Can't set locale to {value}: no such locale has been configured"); } LocalizationSettings.SelectedLocale = locale; } } private LineParser lineParser = new LineParser(); private BuiltInMarkupReplacer builtInReplacer = new BuiltInMarkupReplacer(); void Awake() { lineParser.RegisterMarkerProcessor("select", builtInReplacer); lineParser.RegisterMarkerProcessor("plural", builtInReplacer); lineParser.RegisterMarkerProcessor("ordinal", builtInReplacer); } /// public override async YarnTask GetLocalizedLineAsync(Line line, CancellationToken cancellationToken) { if (stringsTable == null || stringsTable.IsEmpty) { throw new System.InvalidOperationException($"Tried to get localised line for {line.ID}, but no string table has been set."); } var getStringOp = LocalizationSettings.StringDatabase.GetTableEntryAsync(stringsTable.TableReference, line.ID, null, FallbackBehavior.UseFallback); var entry = await YarnTask.WaitForAsyncOperation(getStringOp, cancellationToken); // Attempt to fetch metadata tags for this line from the string // table var metadata = entry.Entry?.SharedEntry.Metadata.GetMetadata(); // Get the text from the entry var text = entry.Entry?.LocalizedValue ?? $"!! Error: Missing localisation for line {line.ID} in string table {entry.Table.LocaleIdentifier}"; string? shadowLineID = metadata?.ShadowLineSource; if (shadowLineID != null) { // This line actually shadows another line. Fetch that line, and // use its text (but not its metadata) var getShadowLineOp = LocalizationSettings.StringDatabase.GetTableEntryAsync(stringsTable.TableReference, shadowLineID, null, FallbackBehavior.UseFallback); var shadowEntry = await YarnTask.WaitForAsyncOperation(getShadowLineOp, cancellationToken); if (shadowEntry.Entry == null) { Debug.LogWarning($"Line {line.ID} shadows line {shadowLineID}, but no such entry was found in the string table {stringsTable.TableReference}"); } else { text = shadowEntry.Entry.LocalizedValue; } } // We now have our text; parse it as markup var markup = lineParser.ParseString(LineParser.ExpandSubstitutions(text, line.Substitutions), this.LocaleCode); // Lastly, attempt to fetch an asset for this line Object? asset = null; if (this.assetTable != null && this.assetTable.IsEmpty == false) { // Fetch the asset for this line, if one is available. var loadOp = LocalizationSettings.AssetDatabase.GetLocalizedAssetAsync(assetTable.TableReference, shadowLineID ?? line.ID, null, FallbackBehavior.UseFallback); asset = await YarnTask.WaitForAsyncOperation(loadOp, cancellationToken); } // Construct the localized line LocalizedLine localizedLine = new LocalizedLine() { Text = markup, TextID = line.ID, Substitutions = line.Substitutions, RawText = text, Metadata = metadata?.tags ?? System.Array.Empty(), Asset = asset, }; return localizedLine; } /// public override void RegisterMarkerProcessor(string attributeName, IAttributeMarkerProcessor markerProcessor) { lineParser.RegisterMarkerProcessor(attributeName, markerProcessor); } /// public override void DeregisterMarkerProcessor(string attributeName) { lineParser.DeregisterMarkerProcessor(attributeName); } /// public override YarnTask PrepareForLinesAsync(IEnumerable lineIDs, CancellationToken cancellationToken) { // Nothing to do. If a user wants to ensure ahead of time that // localized content is already in memory, they should use the Unity // Localization preload support. return YarnTask.CompletedTask; } } } #if UNITY_EDITOR namespace Yarn.Unity.UnityLocalization.Editor { using System; using UnityEditor; [CustomEditor(typeof(UnityLocalisedLineProvider))] internal class UnityLocalisedLineProviderEditor : UnityEditor.Editor { private SerializedProperty? stringsTableProperty; private SerializedProperty? assetTableProperty; /// /// Called by Unity to draw the inspector GUI. /// public override void OnInspectorGUI() { EditorGUILayout.PropertyField(stringsTableProperty); var stringTableName = stringsTableProperty?.FindPropertyRelative("m_TableReference").FindPropertyRelative("m_TableCollectionName").stringValue; if (string.IsNullOrEmpty(stringTableName)) { EditorGUI.indentLevel += 1; EditorGUILayout.HelpBox("Choose a strings table to make this line provider able to deliver line text.", MessageType.Warning); EditorGUI.indentLevel -= 1; } EditorGUILayout.PropertyField(assetTableProperty); serializedObject.ApplyModifiedProperties(); } /// /// Called by Unity when the editor is enabled to set up cached properties. /// protected void OnEnable() { this.stringsTableProperty = serializedObject.FindProperty(nameof(UnityLocalisedLineProvider.stringsTable)); this.assetTableProperty = serializedObject.FindProperty(nameof(UnityLocalisedLineProvider.assetTable)); } } } #endif // UNITY_EDITOR #endif // USE_UNITY_LOCALIZATION