Beispiel #1
0
        /// <summary>
        /// Invoked by the menu system to create a new localization on the
        /// currently selected objects with a given language.
        /// </summary>
        /// <param name="language">The language to use. This must be a
        /// string.</param>
        private void CreateLocalizationWithLanguage(object language)
        {
            if (!(language is string theLanguage))
            {
                throw new ArgumentException("Expected to receive a string", nameof(language));
            }

            if (serializedObject.isEditingMultipleObjects)
            {
                Debug.LogWarning($"Can't create a new {nameof(Localization)} when multiple objects are selected");
                return;
            }

            LocalizationDatabaseUtility.CreateLocalizationWithLanguage(serializedObject, theLanguage);
        }
Beispiel #2
0
        public void YarnImporterUtility_CanCreateLocalizationInLocalizationDatabase()
        {
            // Arrange: Import a yarn script and create a localization
            // database for it
            string fileName = Path.GetRandomFileName();
            string path     = Path.Combine("Assets", fileName + ".yarn");

            createdFilePaths.Add(path);
            File.WriteAllText(path, TestYarnScriptSource);
            AssetDatabase.Refresh();
            var importer = AssetImporter.GetAtPath(path) as YarnImporter;
            var importerSerializedObject = new SerializedObject(importer);

            YarnImporterUtility.CreateNewLocalizationDatabase(importerSerializedObject);
            createdFilePaths.Add(AssetDatabase.GetAssetPath(importer.localizationDatabase));

            var databaseSerializedObject = new SerializedObject(importer.localizationDatabase);

            // Act: Create a new localization CSV file for some new language
            LocalizationDatabaseUtility.CreateLocalizationWithLanguage(databaseSerializedObject, AlternateLocaleCode);
            YarnImporterUtility.CreateLocalizationForLanguageInProgram(importerSerializedObject, AlternateLocaleCode);

            foreach (var loc in importer.localizationDatabase.Localizations)
            {
                createdFilePaths.Add(AssetDatabase.GetAssetPath(loc));
            }

            foreach (var loc in importer.localizations)
            {
                createdFilePaths.Add(AssetDatabase.GetAssetPath(loc.text));
            }

            importer.SaveAndReimport();

            // Assert: Verify that it exists, contains the string table
            // entries we expect, and has the language we expect.
            var expectedLanguages = new HashSet <string> {
                importer.baseLanguageID, AlternateLocaleCode
            }.OrderBy(n => n);
            var foundLanguages = importer.programContainer.localizations.Select(l => l.languageName).OrderBy(n => n);

            CollectionAssert.AreEquivalent(expectedLanguages, foundLanguages, $"The locales should be what we expect to see");
        }
Beispiel #3
0
        public void YarnImporterUtility_CanUpdateLocalizedCSVs_WhenBaseScriptChanges()
        {
            // Arrange: Import a yarn script and create a localization
            // database for it, create an alternate localization for it
            string fileName = Path.GetRandomFileName();
            string path     = Path.Combine("Assets", fileName + ".yarn");

            createdFilePaths.Add(path);
            File.WriteAllText(path, TestYarnScriptSource);
            AssetDatabase.Refresh();
            var importer = AssetImporter.GetAtPath(path) as YarnImporter;
            var importerSerializedObject = new SerializedObject(importer);

            YarnImporterUtility.CreateNewLocalizationDatabase(importerSerializedObject);
            var localizationDatabaseSerializedObject = new SerializedObject(importer.localizationDatabase);

            LocalizationDatabaseUtility.CreateLocalizationWithLanguage(localizationDatabaseSerializedObject, AlternateLocaleCode);

            YarnImporterUtility.CreateLocalizationForLanguageInProgram(importerSerializedObject, AlternateLocaleCode);

            var unmodifiedBaseStringsTable      = StringTableEntry.ParseFromCSV((importerSerializedObject.targetObject as YarnImporter).baseLanguage.text);
            var unmodifiedLocalizedStringsTable = StringTableEntry.ParseFromCSV((importerSerializedObject.targetObject as YarnImporter).localizations.First(l => l.languageName == AlternateLocaleCode).text.text);

            // Act: modify the imported script so that lines are added,
            // changed and deleted, and then update the localized CSV
            // programmatically

            File.WriteAllText(path, TestYarnScriptSourceModified);
            AssetDatabase.Refresh();
            YarnImporterUtility.UpdateLocalizationCSVs(importerSerializedObject);

            var modifiedBaseStringsTable      = StringTableEntry.ParseFromCSV((importerSerializedObject.targetObject as YarnImporter).baseLanguage.text);
            var modifiedLocalizedStringsTable = StringTableEntry.ParseFromCSV((importerSerializedObject.targetObject as YarnImporter).localizations.First(l => l.languageName == AlternateLocaleCode).text.text);

            // Assert: verify the base language string table contains the
            // string table entries we expect, verify the localized string
            // table contains the string table entries we expect

            System.Func <StringTableEntry, string> CompareIDs   = t => t.ID;
            System.Func <StringTableEntry, string> CompareLocks = t => t.Lock;

            var tests = new[] {
Beispiel #4
0
    /// <summary>
    /// Updates every localization .CSV file associated with this .yarn
    /// file.
    /// </summary>
    /// <remarks>
    /// This method updates each localization file by performing the
    /// following operations:
    ///
    /// - Inserts new entries if they're present in the base localization
    /// and not in the translated localization
    ///
    /// - Removes entries if they're present in the translated localization
    /// and not in the base localization
    ///
    /// - Detects if a line in the base localization has changed its Lock
    /// value from when the translated localization was created, and update
    /// its Comment
    /// </remarks>
    /// <param name="serializedObject">A serialized object that represents
    /// a <see cref="YarnImporter"/>.</param>
    internal static void UpdateLocalizationCSVs(SerializedObject serializedObject)
    {
        var localizationDatabaseProperty = serializedObject.FindProperty("localizationDatabase");

        if (serializedObject.isEditingMultipleObjects)
        {
            Debug.LogError($"Can't update localization CSVs: multiple objects are being edited.");
            return;
        }

        var baseLocalizationStrings = GetBaseLanguageStringsForSelectedObject(serializedObject);

        var localizations = (serializedObject.targetObject as YarnImporter).localizations;

        var modifiedFiles = new List <TextAsset>();

        foreach (var loc in localizations)
        {
            var fileWasChanged = UpdateLocalizationFile(baseLocalizationStrings, loc.languageName, loc.text);

            if (fileWasChanged)
            {
                modifiedFiles.Add(loc.text);
            }
        }

        if (modifiedFiles.Count > 0)
        {
            Debug.Log($"Updated the following files: {string.Join(", ", modifiedFiles.Select(f => f.name))}");
        }
        else
        {
            Debug.Log($"No files needed updating.");
            // Update our corresponding localization database.
            if (localizationDatabaseProperty.objectReferenceValue is LocalizationDatabase database)
            {
                LocalizationDatabaseUtility.UpdateContents(database);
            }
        }
    }
Beispiel #5
0
    /// <summary>
    /// Creates a new localization database asset adjacent to one of the
    /// selected objects, configures all selected objects to use it, and
    /// ensures that the project's text language list includes this
    /// program's base language.
    /// </summary>
    /// <param name="serializedObject">A serialized object that represents
    /// a <see cref="YarnImporter"/>.</param>
    internal static void CreateNewLocalizationDatabase(SerializedObject serializedObject)
    {
        if (serializedObject.isEditingMultipleObjects)
        {
            throw new System.InvalidOperationException($"Cannot invoke {nameof(CreateNewLocalizationDatabase)} when editing multiple objects");
        }

        var localizationDatabaseProperty = serializedObject.FindProperty("localizationDatabase");

        var target = serializedObject.targetObjects[0];

        // Figure out where on disk this asset is
        var path      = AssetDatabase.GetAssetPath(target);
        var directory = Path.GetDirectoryName(path);

        // Figure out a new, unique path for the localization we're
        // creating
        var databaseFileName = $"LocalizationDatabase.asset";
        var destinationPath  = Path.Combine(directory, databaseFileName);

        destinationPath = AssetDatabase.GenerateUniqueAssetPath(destinationPath);

        // Create the asset and set it up
        var localizationDatabaseAsset = ScriptableObject.CreateInstance <LocalizationDatabase>();

        // The list of languages we needed to add to the project's text language
        // list
        var languagesAddedToProject = new List <string>();

        // Attach all selected programs to this new database
        foreach (YarnImporter importer in serializedObject.targetObjects)
        {
            if (importer.programContainer != null)
            {
                localizationDatabaseAsset.AddTrackedProgram(importer.programContainer);

                // If this database doesn't currently have a localization
                // for the currently selected program, add one
                var theLanguage = importer.baseLanguageID;

                if (localizationDatabaseAsset.HasLocalization(theLanguage) == false)
                {
                    var localizationPath = Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(databaseFileName)}-{theLanguage}.asset");
                    localizationPath = AssetDatabase.GenerateUniqueAssetPath(localizationPath);

                    // Create the asset and set it up
                    var localizationAsset = ScriptableObject.CreateInstance <Localization>();
                    localizationAsset.LocaleCode = theLanguage;

                    AssetDatabase.CreateAsset(localizationAsset, localizationPath);

                    localizationDatabaseAsset.AddLocalization(localizationAsset);
                }

                // Add this language to the project's text language list if
                // we need to, and remember that we did so
                if (ProjectSettings.TextProjectLanguages.Contains(importer.baseLanguageID) == false)
                {
                    ProjectSettings.TextProjectLanguages.Add(importer.baseLanguageID);
                    languagesAddedToProject.Add(importer.baseLanguageID);
                }
            }
        }

        // Log if we needed to update the project text language list
        if (languagesAddedToProject.Count > 0)
        {
            Debug.Log($"The following {(languagesAddedToProject.Count == 1 ? "language was" : "languages were")} added to the project's text language list: {string.Join(", ", languagesAddedToProject)}. To review this list, choose Edit > Project Settings > Yarn Spinner.");
        }

        // Populate the database's contents
        LocalizationDatabaseUtility.UpdateContents(localizationDatabaseAsset);

        // Save it to disk
        AssetDatabase.CreateAsset(localizationDatabaseAsset, destinationPath);
        AssetDatabase.SaveAssets();
        AssetDatabase.ImportAsset(destinationPath);

        // Associate this localization database with the object.
        localizationDatabaseProperty.objectReferenceValue = localizationDatabaseAsset;

        serializedObject.ApplyModifiedProperties();
    }
Beispiel #6
0
        public override void OnInspectorGUI()
        {
            // If true, at least one of the Localizations in the selected
            // LocalizationDatabases are null references; in this case, the
            // Add New Localization button will be disabled (it will be
            // re-enabled when the empty field is filled)
            bool anyLocalizationsAreNull = false;

            if (localizationsProperty.arraySize == 0)
            {
                EditorGUILayout.HelpBox($"This {ObjectNames.NicifyVariableName(nameof(LocalizationDatabase)).ToLowerInvariant()} has no {ObjectNames.NicifyVariableName(nameof(Localization)).ToLowerInvariant()}s. Create a new one, or add an existing one.", MessageType.Info);
            }

            foreach (SerializedProperty property in localizationsProperty)
            {
                // The locale code for this localization ("en-AU")
                string localeCode = null;

                // The human-friendly code for this localization ("English
                // (Australia)")
                string localeCodeDisplayName = null;

                // If true, a localization asset is present in this
                // property
                bool localizationPresent = false;

                // If true, the locale code for this localization asset
                // exists inside the Cultures class's list
                bool cultureIsValid = false;

                if (property.objectReferenceValue != null)
                {
                    localeCode          = (property.objectReferenceValue as Localization).LocaleCode;
                    localizationPresent = true;

                    cultureIsValid = Cultures.HasCulture(localeCode);

                    if (cultureIsValid)
                    {
                        localeCodeDisplayName = Cultures.GetCulture(localeCode).DisplayName;
                    }
                    else
                    {
                        localeCodeDisplayName = "Invalid";
                    }
                }
                else
                {
                    // This property is empty. We'll end up drawing an
                    // empty object field here; record this so that we know
                    // to disable the 'Add Existing' button later.
                    anyLocalizationsAreNull = true;
                }

                using (new EditorGUILayout.HorizontalScope())
                {
                    // If a localization is present, show the display name
                    // and locale code; otherwise, show null (which will
                    // make the corresponding empty field take up the whole
                    // width)
                    string labelContents;
                    if (localizationPresent)
                    {
                        labelContents = localeCodeDisplayName + $" ({localeCode})";
                    }
                    else
                    {
                        labelContents = "";
                    }

                    // Show the property field for this element in the
                    // array
                    EditorGUILayout.PropertyField(property, new GUIContent(labelContents));

                    // Show a button that removes this element
                    if (GUILayout.Button("-", EditorStyles.miniButton, GUILayout.ExpandWidth(false)))
                    {
                        // Remove the element from the slot in the array...
                        property.DeleteCommand();
                        // ... and remove the empty slot from the array.
                        property.DeleteCommand();
                    }
                }

                // Is a locale code set that's invalid?
                if (localeCode == null)
                {
                    EditorGUILayout.HelpBox($"Drag and drop a {nameof(Localization)} to this field to add it to this localization database.", MessageType.Info);
                }
                else
                {
                    if (cultureIsValid == false)
                    {
                        // A locale code was set, but this locale code
                        // isn't valid. Show a warning.
                        EditorGUILayout.HelpBox($"'{localeCode}' is not a valid locale. This localization's contents won't be available in the game.", MessageType.Warning);
                    }
                    else if (ProjectSettings.TextProjectLanguages.Contains(localeCode) == false)
                    {
                        // The locale is valid, but the project settings
                        // don't include this language. Show a warning (the
                        // user won't be able to select this localization)
                        const string fixButtonLabel = "Add to Language List";

                        EditorGUILayout.HelpBox($"{localeCodeDisplayName} is not in this project's language list. This localization's contents won't be available in the game.\n\nClick {fixButtonLabel} to fix this issue.", MessageType.Warning);

                        if (GUILayout.Button(fixButtonLabel))
                        {
                            ProjectSettings.AddNewTextLanguage(localeCode);

                            // This will resolve the error, so we'll
                            // immediately repaint
                            Repaint();
                        }

                        // Nice little space to visually associate the
                        // 'add' button with the field and reduce confusion
                        EditorGUILayout.Space();
                    }
                }
            }

            // Show the buttons for adding and creating localizations only
            // if we're not editing multiple databases.
            if (serializedObject.isEditingMultipleObjects == false)
            {
                // Disable the 'add existing' button if there's already an
                // empty field. (Clicking this button adds a new empty field,
                // so we don't want to end up creating multiples.)
                using (new EditorGUI.DisabledScope(anyLocalizationsAreNull))
                {
                    // Show the 'add existing' button, which adds a new
                    // empty field for the user to drop an existing
                    // Localization asset into.
                    if (GUILayout.Button("Add Existing Localisation"))
                    {
                        localizationsProperty.InsertArrayElementAtIndex(localizationsProperty.arraySize);
                        localizationsProperty.GetArrayElementAtIndex(localizationsProperty.arraySize - 1).objectReferenceValue = null;
                    }
                }

                // Show the 'create new' button, which displays the menu of
                // available languages; selecting a language causes a new
                // localization asset to be created with that language, and
                // adds it to this database.
                if (GUILayout.Button("Create New Localization"))
                {
                    var languageMenu = CreateLanguageMenu();

                    languageMenu.ShowAsContext();
                }
            }

            GUILayout.Space(EditorGUIUtility.singleLineHeight);

            // Show information about the scripts we're pulling data from,
            // and offer to update the database now
            if (trackedProgramsProperty.arraySize == 0)
            {
                EditorGUILayout.HelpBox("No Yarn scripts currently use this database.\n\nTo make a Yarn script use this database, select one, and set its Localization Database to this file.", MessageType.Info);
            }
            else
            {
                if (serializedObject.isEditingMultipleObjects == false)
                {
                    EditorGUILayout.LabelField("Uses lines from:");

                    EditorGUI.indentLevel += 1;

                    // List every tracked program, but disable it (we don't
                    // change them here, they're changed in the inspector for
                    // the Yarn script.)
                    using (new EditorGUI.DisabledScope(true))
                    {
                        foreach (SerializedProperty trackedProgramProperty in trackedProgramsProperty)
                        {
                            EditorGUILayout.PropertyField(trackedProgramProperty, new GUIContent());
                        }
                    }

                    EditorGUI.indentLevel -= 1;

                    EditorGUILayout.Space();

                    EditorGUILayout.HelpBox("This database will automatically update when the contents of these scripts change. If you modify the .csv files for other translations, modify any locale-specific assets, or if you need to manually update the database, click Update Database.", MessageType.Info);
                }

                if (GUILayout.Button("Update Database"))
                {
                    foreach (LocalizationDatabase target in serializedObject.targetObjects)
                    {
                        LocalizationDatabaseUtility.UpdateContents(target);
                    }
                }

#if ADDRESSABLES
                // Give a helpful note if addressables are availalbe, but
                // haven't been set up. (In this circumstance,
                // Localizations won't be able to store references to the
                // assets they find.)
                if (AddressableAssetSettingsDefaultObject.SettingsExists == false)
                {
                    EditorGUILayout.HelpBox("The Addressable Assets package has been added, but it hasn't been set up yet. Assets associated with lines won't be included in this database.\n\nTo set up Addressable Assets, choose Window > Asset Management > Addressables > Groups.", MessageType.Info);
                }
#endif
            }

            serializedObject.ApplyModifiedProperties();
        }
Beispiel #7
0
        // Detects when a YarnProgram has been imported (either created or
        // modified), and checks to see if its importer is configured to
        // associate it with a LocalizationDatabase. If it is, the
        // LocalizationDatabase is updated to include this program in its
        // TrackedPrograms collection. Finally, the LocalizationDatabase is
        // made to update its contents.
        //
        // We do this in a post-processor rather than in the importer
        // itself, because assets created in an importer don't actually
        // "exist" (as far as Unity is concerned) until after the import
        // process completes, so references to that asset aren't valid
        // (i.e. it hasn't been assigned a GUID yet).
        static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
        {
            // Get the list of paths to assets that we've just imported
            // whose main asset is a YarnProgram. (If there aren't any,
            // this method has nothing to do.)
            var importedYarnAssets = importedAssets.Where(path => AssetDatabase.GetMainAssetTypeAtPath(path)?.IsAssignableFrom(typeof(YarnProgram)) ?? false);

            if (importedYarnAssets.Count() == 0)
            {
                return;
            }

            // Tracks all databases that are affected by the  Yarn scripts
            // that we just imported
            var impactedDatabases = new HashSet <LocalizationDatabase>();

            foreach (var importedPath in importedYarnAssets)
            {
                var importer = AssetImporter.GetAtPath(importedPath) as YarnImporter;

                // Verify that we have the right kind of importer that we
                // expect.
                if (importer == null)
                {
                    Debug.Log($"{nameof(YarnProgram)} at {importedPath}'s importer is not a {nameof(YarnImporter)}. This probably indicates a problem elsewhere in your setup.");
                    continue;
                }

                // Try and get the localization database!
                var database = importer.localizationDatabase;

                if (database == null)
                {
                    // This program has no localization database to
                    // associate with, so nothing to do here.
                    continue;
                }

                var trackedPrograms = importer.localizationDatabase.TrackedPrograms;

                // The database is using data from this program if any of
                // the programs in its list have a path that match this
                // asset. (Doing it this way means we don't need to
                // deserialize the asset at importedPath unless we really
                // need to.)
                var databaseIsTrackingThisProgram = trackedPrograms.Select(p => AssetDatabase.GetAssetPath(p))
                                                    .Contains(importedPath);

                if (databaseIsTrackingThisProgram == false)
                {
                    // The YarnProgram wants the database to have it in its
                    // TrackedPrograms list, but it's not there. We need to
                    // add it.

                    // First, import the program, so we can add the
                    // reference.
                    var program = AssetDatabase.LoadAssetAtPath <YarnProgram>(importedPath);

                    if (program == null)
                    {
                        // The program failed to be loaded at this path. It
                        // probably failed to compile, so we can't add it
                        // to the localized database.
                        continue;
                    }

                    // Add this program to the list.
                    importer.localizationDatabase.AddTrackedProgram(program);
                }

                // This database needs to be updated.
                impactedDatabases.Add(importer.localizationDatabase);
            }

            if (impactedDatabases.Count > 0)
            {
                foreach (var db in impactedDatabases)
                {
                    // Make the database update its contents
                    LocalizationDatabaseUtility.UpdateContents(db);
                }

                // Save any changed localization databases. (This will trigger
                // this method to be called again, but none of the paths that
                // we just worked with will have changed, so it won't trigger a
                // recursion.)
                AssetDatabase.SaveAssets();
            }
        }