Skip to content

Commit

Permalink
Use a post-processor to update Unity string tables on Yarn Project im…
Browse files Browse the repository at this point in the history
…port

Due to a bug in Unity, ScriptedImporter objects aren't able to interact
with ScriptableObject instances during import, and Unity string tables
are scriptable objects. To work around this, we update the string table
after import is complete, using a post-processor.
  • Loading branch information
desplesda committed Feb 4, 2025
1 parent 884b24e commit ea1c610
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 41 deletions.
38 changes: 32 additions & 6 deletions Editor/Editors/YarnProjectImporterEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public class YarnProjectImporterEditor : ScriptedImporterEditor

#if USE_UNITY_LOCALIZATION
private SerializedProperty useUnityLocalisationSystemProperty;
private SerializedProperty unityLocalisationTableCollectionProperty;
private SerializedProperty unityLocalisationTableCollectionGUIDProperty;
#endif

private bool AnyModifications
Expand All @@ -80,13 +80,14 @@ private bool AnyModifications
}
}

private bool AnyLocalisationModifications => LocalisationsAddedOrRemoved || localizationEntryFields.Any(f => f.IsModified) || BaseLanguageNameModified;
private bool AnyLocalisationModifications => LocalisationsAddedOrRemoved || localizationEntryFields.Any(f => f.IsModified) || BaseLanguageNameModified || StringTableModified;

private bool AnySourceFileModifications => SourceFilesAddedOrRemoved || sourceEntryFields.Any(f => f.IsModified);

private bool LocalisationsAddedOrRemoved = false;
private bool BaseLanguageNameModified = false;
private bool SourceFilesAddedOrRemoved = false;
private bool StringTableModified = false;

public override void OnEnable()
{
Expand All @@ -96,7 +97,7 @@ public override void OnEnable()

#if USE_UNITY_LOCALIZATION
useUnityLocalisationSystemProperty = serializedObject.FindProperty(nameof(YarnProjectImporter.UseUnityLocalisationSystem));
unityLocalisationTableCollectionProperty = serializedObject.FindProperty(nameof(YarnProjectImporter.unityLocalisationStringTableCollection));
unityLocalisationTableCollectionGUIDProperty = serializedObject.FindProperty(nameof(YarnProjectImporter.unityLocalisationStringTableCollectionGUID));
#endif

generateVariablesSourceFileProperty = serializedObject.FindProperty(nameof(YarnProjectImporter.generateVariablesSourceFile));
Expand Down Expand Up @@ -212,6 +213,7 @@ protected override void Apply()
BaseLanguageNameModified = false;
SourceFilesAddedOrRemoved = false;
LocalisationsAddedOrRemoved = false;
StringTableModified = false;

data.SaveToFile(importer.assetPath);

Expand Down Expand Up @@ -298,7 +300,14 @@ public override VisualElement CreateInspectorGUI()

#if USE_UNITY_LOCALIZATION
var useUnityLocalisationSystemField = new PropertyField(useUnityLocalisationSystemProperty);
var unityLocalisationTableCollectionField = new PropertyField(unityLocalisationTableCollectionProperty);

// References to string table collections are stored as GUIDs,
// because ScriptedImporters can't refer to ScriptableObjects
// directly without causing drama. To preserve a good user
// experience, we'll add and manage an ObjectField directly.
var unityLocalisationTableCollectionField = new ObjectField("String Table Collection");
unityLocalisationTableCollectionField.objectType = typeof(StringTableCollection);
unityLocalisationTableCollectionField.SetValueWithoutNotify(yarnProjectImporter.UnityLocalisationStringTableCollection);
#endif

localisationFieldsContainer = new VisualElement();
Expand Down Expand Up @@ -449,7 +458,7 @@ void UpdateLocalizationVisibility()

void UpdateUnityTableCollectionEmptyWarningVisibility()
{
SetElementVisible(emptyTableCollectionWarning, unityLocalisationTableCollectionProperty.objectReferenceValue == null);
SetElementVisible(emptyTableCollectionWarning, string.IsNullOrEmpty(unityLocalisationTableCollectionGUIDProperty.stringValue));
}

UpdateLocalizationVisibility();
Expand All @@ -460,8 +469,25 @@ void UpdateUnityTableCollectionEmptyWarningVisibility()
UpdateLocalizationVisibility();
});

unityLocalisationTableCollectionField.RegisterValueChangeCallback(evt =>

unityLocalisationTableCollectionField.RegisterValueChangedCallback(evt =>
{
// When the localisation table changes, get the GUID for it and
// store it in the property.

if (evt.newValue != null && AssetDatabase.TryGetGUIDAndLocalFileIdentifier(evt.newValue, out string guid, out long _))
{
unityLocalisationTableCollectionGUIDProperty.stringValue = guid;
}
else
{
// The object is null, or a GUID for it can't be found.
unityLocalisationTableCollectionGUIDProperty.stringValue = string.Empty;
}

// Flag that we've changed our importer's settings.
StringTableModified = true;

UpdateUnityTableCollectionEmptyWarningVisibility();
});
#endif
Expand Down
161 changes: 132 additions & 29 deletions Editor/Importers/YarnProjectImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,54 @@ public SerializedDeclaration(Declaration decl)

#if USE_UNITY_LOCALIZATION
public bool UseUnityLocalisationSystem = false;
public StringTableCollection unityLocalisationStringTableCollection;

// Scripted importers can't have direct references to scriptable
// objects, so we'll store the reference as a string containing the
// GUID.
public string? unityLocalisationStringTableCollectionGUID;

private StringTableCollection? _cachedStringTableCollection;

/// <summary>
/// Gets or sets the Unity Localization string table collection
/// associated with this importer.
/// </summary>
public StringTableCollection? UnityLocalisationStringTableCollection
{
get
{
if (_cachedStringTableCollection == null)
{
if (!string.IsNullOrEmpty(unityLocalisationStringTableCollectionGUID))
{
var assetPath = AssetDatabase.GUIDToAssetPath(unityLocalisationStringTableCollectionGUID);

if (!string.IsNullOrEmpty(assetPath))
{
_cachedStringTableCollection = AssetDatabase.LoadAssetAtPath<StringTableCollection>(assetPath);
}
}
}
return _cachedStringTableCollection;
}
set
{
if (value == null)
{
unityLocalisationStringTableCollectionGUID = string.Empty;
_cachedStringTableCollection = null;
return;
}

if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(value, out var guid, out long _))
{
throw new System.InvalidOperationException($"String table collection {value.name} has no GUID - is it not an asset stored on disk?");
}

unityLocalisationStringTableCollectionGUID = guid;
_cachedStringTableCollection = value;
}
}
#endif

/// <summary>
Expand Down Expand Up @@ -202,6 +249,38 @@ public bool GetProjectReferencesYarnFile(YarnImporter yarnImporter)
}
}

/// <summary>
/// Gets the Yarn string table produced as a result of compiling the Yarn
/// Project, or <see langword="null"/> if no string table could be produced.
/// </summary>
private Dictionary<string, StringInfo>? GetYarnStringTable()
{
Project project;
try
{
project = Project.LoadFromFile(this.assetPath);
}
catch (System.Exception)
{
return null;
}

var job = CompilationJob.CreateFromFiles(project.SourceFiles);
job.LanguageVersion = project.FileVersion;
job.CompilationType = CompilationJob.Type.StringsOnly;

try
{
var compilationResult = Compiler.Compiler.Compile(job);

return new(compilationResult.StringTable);
}
catch (System.Exception)
{
return null;
}
}

/// <summary>
/// Called by Unity to import an asset.
/// </summary>
Expand Down Expand Up @@ -416,7 +495,8 @@ public override void OnImportAsset(AssetImportContext ctx)
#if USE_UNITY_LOCALIZATION
if (UseUnityLocalisationSystem)
{
AddStringTableEntries(compilationResult, this.unityLocalisationStringTableCollection, project);
// Mark that this project uses Unity Localization; we'll
// populate the string table later, in a post-processor.
projectAsset.localizationType = LocalizationType.Unity;
}
else
Expand Down Expand Up @@ -1005,21 +1085,15 @@ private void CreateYarnInternalLocalizationAssets(AssetImportContext ctx, YarnPr
}

#if USE_UNITY_LOCALIZATION
private void AddStringTableEntries(CompilationResult compilationResult, StringTableCollection unityLocalisationStringTableCollection, Yarn.Compiler.Project project)
private static void AddStringTableEntries(IDictionary<string, StringInfo> stringTable, StringTableCollection unityLocalisationStringTableCollection, string baseLanguage)
{
if (unityLocalisationStringTableCollection == null)
{
Debug.LogError("Unable to generate String Table Entries as the string collection is null");
return;
}

// Get the Unity string table corresponding to the Yarn Project's
// base language. If a table can't be found for the language but can
// be for the language's parent, use that. Otherwise, return null.
StringTable FindBaseLanguageStringTable()
StringTable? FindBaseLanguageStringTable(string baseLanguage)
{
StringTable baseLanguageStringTable = unityLocalisationStringTableCollection.StringTables
.FirstOrDefault(t => t.LocaleIdentifier == project.BaseLanguage);
.FirstOrDefault(t => t.LocaleIdentifier == baseLanguage);

if (baseLanguageStringTable != null)
{
Expand All @@ -1030,10 +1104,10 @@ StringTable FindBaseLanguageStringTable()
// code of our Yarn Project's base language. Maybe we can try to
// find a string table for our base language's parent.

System.Globalization.CultureInfo defaultCulture = null;
System.Globalization.CultureInfo? defaultCulture = null;
try
{
defaultCulture = new System.Globalization.CultureInfo(project.BaseLanguage);
defaultCulture = new System.Globalization.CultureInfo(baseLanguage);
}
catch (System.Globalization.CultureNotFoundException)
{
Expand All @@ -1055,15 +1129,15 @@ StringTable FindBaseLanguageStringTable()
return defaultNeutralStringTable;
}

var unityStringTable = FindBaseLanguageStringTable();
var unityStringTable = FindBaseLanguageStringTable(baseLanguage);

if (unityStringTable == null)
{
Debug.LogWarning($"Unable to find a locale in the string table that matches the default locale {project.BaseLanguage}");
Debug.LogWarning($"Unable to find a locale in the string table that matches the default locale {baseLanguage}");
return;
}

foreach (var yarnEntry in compilationResult.StringTable)
foreach (var yarnEntry in stringTable)
{
// Grab the data that we'll put in the string table
var lineID = yarnEntry.Key;
Expand Down Expand Up @@ -1113,6 +1187,8 @@ StringTable FindBaseLanguageStringTable()
// as dirty.
EditorUtility.SetDirty(unityStringTable);
EditorUtility.SetDirty(unityStringTable.SharedData);
EditorUtility.SetDirty(unityLocalisationStringTableCollection);
EditorUtility.SetDirty(LocalizationEditorSettings.ActiveLocalizationSettings);
return;

}
Expand Down Expand Up @@ -1161,18 +1237,6 @@ internal bool CanGenerateStringsTable
}
}

private CompilationResult? CompileStringsOnly()
{
var paths = GetProject().SourceFiles;

var job = CompilationJob.CreateFromFiles(paths);
job.CompilationType = CompilationJob.Type.StringsOnly;

return Compiler.Compiler.Compile(job);


}

internal CompilationJob GetCompilationJob()
{
var project = GetProject();
Expand Down Expand Up @@ -1326,11 +1390,50 @@ private string GenerateCommentWithLineMetadata(string[] metadata)
/// </summary>
/// <param name="metadata">The array with line metadata.</param>
/// <returns>An IEnumerable with any line ID entries removed.</returns>
private IEnumerable<string> RemoveLineIDFromMetadata(string[] metadata)
private static IEnumerable<string> RemoveLineIDFromMetadata(string[] metadata)
{
return metadata.Where(x => !x.StartsWith("line:"));
}

#if USE_UNITY_LOCALIZATION
/// <summary>
/// Attempts to populate the <see cref="StringTableCollection"/>
/// associated with this Yarn Project Importer using strings found in
/// the project's Yarn scripts.
/// </summary>
/// <exception cref="System.InvalidOperationException">Thrown when <see
/// cref="UseUnityLocalisationSystem"/> is <see
/// langword="false"/>.</exception>
internal void AddStringsToUnityLocalization()
{
if (UseUnityLocalisationSystem == false)
{
throw new System.InvalidOperationException($"Can't add strings to Unity Localization: project {assetPath} does not use Unity Localization.");
}

// Get the Yarn string table from the project
Dictionary<string, StringInfo>? table = GetYarnStringTable();

if (table == null || ImportData == null)
{
// No lines available, or importer has not successfully imported
return;
}

// Get the string table collection from the importer
StringTableCollection? tableCollection = UnityLocalisationStringTableCollection;

if (tableCollection == null)
{
Debug.LogError("Unable to generate String Table Entries as the string collection is null", (YarnProjectImporter?)this);
return;
}

// Populate the string table collection from the Yarn strings
AddStringTableEntries(table, tableCollection, ImportData.baseLanguageName);
}
#endif

/// <summary>
/// A placeholder string that may be used in Yarn Project files that
/// represents the root path of the Unity project (that is, the
Expand Down
48 changes: 48 additions & 0 deletions Editor/Importers/YarnProjectUnityLocalizationUpdater.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#if USE_UNITY_LOCALIZATION

using System.Linq;
using UnityEditor;
using UnityEditor.Callbacks;

#nullable enable

namespace Yarn.Unity.Editor
{
/// <summary>
/// An asset post processor that updates Unity Localization string tables
/// when a Yarn Project that uses them is imported.
/// </summary>
/// <remarks>
/// Due to a bug in Unity, <see cref="ScriptedImporter"/> objects aren't
/// able to interact with ScriptableObject instances during import, and
/// Unity string tables are scriptable objects. To work around this, we
/// update the string table after import is complete, using a
/// post-processor.
/// </remarks>
internal class YarnProjectUnityLocalizationUpdater : AssetPostprocessor
{
[RunAfterPackage("com.unity.localization")]
public static void OnPostprocessAllAssets(string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths,
bool didDomainReload)
{
// Get all importers for Yarn projects that were just imported
var importedYarnProjectAssets = importedAssets
.Select(path => AssetImporter.GetAtPath(path))
.OfType<YarnProjectImporter>();

foreach (var importer in importedYarnProjectAssets)
{
// If the importer uses Unity Localization, get it to update its
// table
if (importer.UseUnityLocalisationSystem)
{
importer.AddStringsToUnityLocalization();
}
}
}
}
}
#endif
11 changes: 11 additions & 0 deletions Editor/Importers/YarnProjectUnityLocalizationUpdater.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ea1c610

Please sign in to comment.