/// <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);
            }
        }
    }
    /// <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();
    }
Exemple #3
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();
        }
Exemple #4
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();
            }
        }