You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

661 lines
24 KiB

6 months ago
@tool
extends DialogicSettingsPage
## Settings tab that allows enabeling and updating translation csv-files.
enum TranslationModes {PER_PROJECT, PER_TIMELINE, NONE}
enum SaveLocationModes {INSIDE_TRANSLATION_FOLDER, NEXT_TO_TIMELINE, NONE}
var loading := false
@onready var settings_editor :Control = find_parent('Settings')
## The default CSV filename that contains the translations for character
## properties.
const DEFAULT_CHARACTER_CSV_NAME := "dialogic_character_translations.csv"
## The default CSV filename that contains the translations for timelines.
## Only used when all timelines are supposed to be translated in one file.
const DEFAULT_TIMELINE_CSV_NAME := "dialogic_timeline_translations.csv"
const DEFAULT_GLOSSARY_CSV_NAME := "dialogic_glossary_translations.csv"
const _USED_LOCALES_SETTING := "dialogic/translation/locales"
## Contains translation changes that were made during the last update.
## Unique locales that will be set after updating the CSV files.
var _unique_locales := []
func _get_icon() -> Texture2D:
return get_theme_icon("Translation", "EditorIcons")
func _is_feature_tab() -> bool:
return true
func _ready() -> void:
%TransEnabled.toggled.connect(store_changes)
%OrigLocale.get_suggestions_func = get_locales
%OrigLocale.resource_icon = get_theme_icon("Translation", "EditorIcons")
%OrigLocale.value_changed.connect(store_changes)
%TestingLocale.get_suggestions_func = get_locales
%TestingLocale.resource_icon = get_theme_icon("Translation", "EditorIcons")
%TestingLocale.value_changed.connect(store_changes)
%TransFolderPicker.value_changed.connect(store_changes)
%AddSeparatorEnabled.toggled.connect(store_changes)
%SaveLocationMode.item_selected.connect(store_changes)
%TransMode.item_selected.connect(store_changes)
%UpdateCsvFiles.pressed.connect(_on_update_translations_pressed)
%UpdateCsvFiles.icon = get_theme_icon("Add", "EditorIcons")
%CollectTranslations.pressed.connect(collect_translations)
%CollectTranslations.icon = get_theme_icon("File", "EditorIcons")
%TransRemove.pressed.connect(_on_erase_translations_pressed)
%TransRemove.icon = get_theme_icon("Remove", "EditorIcons")
%UpdateConfirmationDialog.add_button("Keep old & Generate new", false, "generate_new")
%UpdateConfirmationDialog.custom_action.connect(_on_custom_action)
_verify_translation_file()
func _on_custom_action(action: String) -> void:
if action == "generate_new":
update_csv_files()
func _refresh() -> void:
loading = true
%TransEnabled.button_pressed = ProjectSettings.get_setting('dialogic/translation/enabled', false)
%TranslationSettings.visible = %TransEnabled.button_pressed
%OrigLocale.set_value(ProjectSettings.get_setting('dialogic/translation/original_locale', TranslationServer.get_tool_locale()))
%TransMode.select(ProjectSettings.get_setting('dialogic/translation/file_mode', 1))
%TransFolderPicker.set_value(ProjectSettings.get_setting('dialogic/translation/translation_folder', ''))
%TestingLocale.set_value(ProjectSettings.get_setting('internationalization/locale/test', ''))
%AddSeparatorEnabled.button_pressed = ProjectSettings.get_setting('dialogic/translation/add_separator', false)
_verify_translation_file()
loading = false
func store_changes(_fake_arg: Variant = null, _fake_arg2: Variant = null) -> void:
if loading:
return
_verify_translation_file()
ProjectSettings.set_setting('dialogic/translation/enabled', %TransEnabled.button_pressed)
%TranslationSettings.visible = %TransEnabled.button_pressed
ProjectSettings.set_setting('dialogic/translation/original_locale', %OrigLocale.current_value)
ProjectSettings.set_setting('dialogic/translation/file_mode', %TransMode.selected)
ProjectSettings.set_setting('dialogic/translation/translation_folder', %TransFolderPicker.current_value)
ProjectSettings.set_setting('internationalization/locale/test', %TestingLocale.current_value)
ProjectSettings.set_setting('dialogic/translation/save_mode', %SaveLocationMode.selected)
ProjectSettings.set_setting('dialogic/translation/add_separator', %AddSeparatorEnabled.button_pressed)
ProjectSettings.save()
## Checks whether the translation folder path is required.
## If it is, disables the "Update CSV files" button and shows a warning.
##
## The translation folder path is required when either of the following is true:
## - The translation mode is set to "Per Project".
## - The save location mode is set to "Inside Translation Folder".
func _verify_translation_file() -> void:
var translation_folder: String = %TransFolderPicker.current_value
var file_mode: TranslationModes = %TransMode.selected
if file_mode == TranslationModes.PER_PROJECT:
%SaveLocationMode.disabled = true
else:
%SaveLocationMode.disabled = false
var valid_translation_folder := (!translation_folder.is_empty()
and DirAccess.dir_exists_absolute(translation_folder))
%UpdateCsvFiles.disabled = not valid_translation_folder
var status_message := ""
if not valid_translation_folder:
status_message += "⛔ Requires valid translation folder to translate character names"
if file_mode == TranslationModes.PER_PROJECT:
status_message += " and the project CSV file."
else:
status_message += "."
%StatusMessage.text = status_message
func get_locales(_filter: String) -> Dictionary:
var suggestions := {}
suggestions['Default'] = {'value':'', 'tooltip':"Will use the fallback locale set in the project settings."}
suggestions[TranslationServer.get_tool_locale()] = {'value':TranslationServer.get_tool_locale()}
var used_locales: Array = ProjectSettings.get_setting(_USED_LOCALES_SETTING, TranslationServer.get_all_languages())
for locale: String in used_locales:
var language_name := TranslationServer.get_language_name(locale)
# Invalid locales return an empty String.
if language_name.is_empty():
continue
suggestions[locale] = { 'value': locale, 'tooltip': language_name }
return suggestions
func _on_update_translations_pressed() -> void:
var save_mode: SaveLocationModes = %SaveLocationMode.selected
var file_mode: TranslationModes = %TransMode.selected
var translation_folder: String = %TransFolderPicker.current_value
var old_save_mode: SaveLocationModes = ProjectSettings.get_setting('dialogic/translation/intern/save_mode', save_mode)
var old_file_mode: TranslationModes = ProjectSettings.get_setting('dialogic/translation/intern/file_mode', file_mode)
var old_translation_folder: String = ProjectSettings.get_setting('dialogic/translation/intern/translation_folder', translation_folder)
if (old_save_mode == save_mode
and old_file_mode == file_mode
and old_translation_folder == translation_folder):
update_csv_files()
return
%UpdateConfirmationDialog.popup_centered()
## Used by the dialog to inform that the settings were changed.
func _delete_and_update() -> void:
erase_translations()
update_csv_files()
## Creates or updates the glossary CSV files.
func _handle_glossary_translation(
csv_data: CsvUpdateData,
save_location_mode: SaveLocationModes,
translation_mode: TranslationModes,
translation_folder_path: String,
orig_locale: String) -> void:
var glossary_csv: DialogicCsvFile = null
var glossary_paths: Array = ProjectSettings.get_setting('dialogic/glossary/glossary_files', [])
var add_separator_lines: bool = ProjectSettings.get_setting('dialogic/translation/add_separator', false)
for glossary_path: String in glossary_paths:
if glossary_csv == null:
var csv_name := ""
# Get glossary CSV file name.
match translation_mode:
TranslationModes.PER_PROJECT:
csv_name = DEFAULT_GLOSSARY_CSV_NAME
TranslationModes.PER_TIMELINE:
var glossary_name: String = glossary_path.trim_suffix('.tres')
var path_parts := glossary_name.split("/")
var file_name := path_parts[-1]
csv_name = "dialogic_" + file_name + '_translation.csv'
var glossary_csv_path := ""
# Get glossary CSV file path.
match save_location_mode:
SaveLocationModes.INSIDE_TRANSLATION_FOLDER:
glossary_csv_path = translation_folder_path.path_join(csv_name)
SaveLocationModes.NEXT_TO_TIMELINE:
glossary_csv_path = glossary_path.get_base_dir().path_join(csv_name)
# Create or update glossary CSV file.
glossary_csv = DialogicCsvFile.new(glossary_csv_path, orig_locale, add_separator_lines)
if (glossary_csv.is_new_file):
csv_data.new_glossaries += 1
else:
csv_data.updated_glossaries += 1
var glossary: DialogicGlossary = load(glossary_path)
glossary_csv.collect_lines_from_glossary(glossary)
glossary_csv.add_translation_keys_to_glossary(glossary)
ResourceSaver.save(glossary)
#If per-file mode is used, save this csv and begin a new one
if translation_mode == TranslationModes.PER_TIMELINE:
glossary_csv.update_csv_file_on_disk()
glossary_csv = null
# If a Per-Project glossary is still open, we need to save it.
if glossary_csv != null:
glossary_csv.update_csv_file_on_disk()
glossary_csv = null
## Keeps information about the amount of new and updated CSV rows and what
## resources were populated with translation IDs.
## The final data can be used to display a status message.
class CsvUpdateData:
var new_events := 0
var updated_events := 0
var new_timelines := 0
var updated_timelines := 0
var new_names := 0
var updated_names := 0
var new_glossaries := 0
var updated_glossaries := 0
var new_glossary_entries := 0
var updated_glossary_entries := 0
func update_csv_files() -> void:
_unique_locales = []
var orig_locale: String = ProjectSettings.get_setting('dialogic/translation/original_locale', '').strip_edges()
var save_location_mode: SaveLocationModes = ProjectSettings.get_setting('dialogic/translation/save_mode', SaveLocationModes.NEXT_TO_TIMELINE)
var translation_mode: TranslationModes = ProjectSettings.get_setting('dialogic/translation/file_mode', TranslationModes.PER_PROJECT)
var translation_folder_path: String = ProjectSettings.get_setting('dialogic/translation/translation_folder', 'res://')
var add_separator_lines: bool = ProjectSettings.get_setting('dialogic/translation/add_separator', false)
var csv_data := CsvUpdateData.new()
if orig_locale.is_empty():
orig_locale = ProjectSettings.get_setting('internationalization/locale/fallback')
ProjectSettings.set_setting('dialogic/translation/intern/save_mode', save_location_mode)
ProjectSettings.set_setting('dialogic/translation/intern/file_mode', translation_mode)
ProjectSettings.set_setting('dialogic/translation/intern/translation_folder', translation_folder_path)
var current_timeline := _close_active_timeline()
var csv_per_project: DialogicCsvFile = null
var per_project_csv_path := translation_folder_path.path_join(DEFAULT_TIMELINE_CSV_NAME)
if translation_mode == TranslationModes.PER_PROJECT:
csv_per_project = DialogicCsvFile.new(per_project_csv_path, orig_locale, add_separator_lines)
if (csv_per_project.is_new_file):
csv_data.new_timelines += 1
else:
csv_data.updated_timelines += 1
# Iterate over all timelines.
# Create or update CSV files.
# Transform the timeline into translatable lines and collect into the CSV file.
for timeline_path: String in DialogicResourceUtil.list_resources_of_type('.dtl'):
var csv_file: DialogicCsvFile = csv_per_project
# Swap the CSV file to the Per Timeline one.
if translation_mode == TranslationModes.PER_TIMELINE:
var per_timeline_path: String = timeline_path.trim_suffix('.dtl')
var path_parts := per_timeline_path.split("/")
var timeline_name: String = path_parts[-1]
# Adjust the file path to the translation location mode.
if save_location_mode == SaveLocationModes.INSIDE_TRANSLATION_FOLDER:
var prefixed_timeline_name := "dialogic_" + timeline_name
per_timeline_path = translation_folder_path.path_join(prefixed_timeline_name)
per_timeline_path += '_translation.csv'
csv_file = DialogicCsvFile.new(per_timeline_path, orig_locale, false)
csv_data.new_timelines += 1
# Load and process timeline, turn events into resources.
var timeline: DialogicTimeline = load(timeline_path)
if timeline.events.size() == 0:
print_rich("[color=yellow]Empty timeline, skipping: " + timeline_path + "[/color]")
continue
timeline.process()
# Collect timeline into CSV.
csv_file.collect_lines_from_timeline(timeline)
# in case new translation_id's were added, we save the timeline again
timeline.set_meta("timeline_not_saved", true)
ResourceSaver.save(timeline, timeline_path)
if translation_mode == TranslationModes.PER_TIMELINE:
csv_file.update_csv_file_on_disk()
csv_data.new_events += csv_file.new_rows
csv_data.updated_events += csv_file.updated_rows
_handle_glossary_translation(
csv_data,
save_location_mode,
translation_mode,
translation_folder_path,
orig_locale
)
_handle_character_names(
csv_data,
orig_locale,
translation_folder_path,
add_separator_lines
)
if translation_mode == TranslationModes.PER_PROJECT:
csv_per_project.update_csv_file_on_disk()
_silently_open_timeline(current_timeline)
# Trigger reimport.
find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
var status_message := "Events created {new_events} found {updated_events}
Names created {new_names} found {updated_names}
CSVs created {new_timelines} found {updated_timelines}
Glossary created {new_glossaries} found {updated_glossaries}
Entries created {new_glossary_entries} found {updated_glossary_entries}"
var status_message_args := {
'new_events': csv_data.new_events,
'updated_events': csv_data.updated_events,
'new_timelines': csv_data.new_timelines,
'updated_timelines': csv_data.updated_timelines,
'new_glossaries': csv_data.new_glossaries,
'updated_glossaries': csv_data.updated_glossaries,
'new_names': csv_data.new_names,
'updated_names': csv_data.updated_names,
'new_glossary_entries': csv_data.new_glossary_entries,
'updated_glossary_entries': csv_data.updated_glossary_entries,
}
%StatusMessage.text = status_message.format(status_message_args)
ProjectSettings.set_setting(_USED_LOCALES_SETTING, _unique_locales)
## Iterates over all character resource files and creates or updates CSV files
## that contain the translations for character properties.
## This will save each character resource file to disk.
func _handle_character_names(
csv_data: CsvUpdateData,
original_locale: String,
translation_folder_path: String,
add_separator_lines: bool) -> void:
var names_csv_path := translation_folder_path.path_join(DEFAULT_CHARACTER_CSV_NAME)
var character_name_csv: DialogicCsvFile = DialogicCsvFile.new(names_csv_path,
original_locale,
add_separator_lines
)
var all_characters := {}
for character_path: String in DialogicResourceUtil.list_resources_of_type('.dch'):
var character: DialogicCharacter = load(character_path)
if character._translation_id.is_empty():
csv_data.new_names += 1
else:
csv_data.updated_names += 1
var translation_id := character.get_set_translation_id()
all_characters[translation_id] = character
ResourceSaver.save(character)
character_name_csv.collect_lines_from_characters(all_characters)
character_name_csv.update_csv_file_on_disk()
func collect_translations() -> void:
var translation_files := []
var translation_mode: TranslationModes = ProjectSettings.get_setting('dialogic/translation/file_mode', TranslationModes.PER_PROJECT)
if translation_mode == TranslationModes.PER_TIMELINE:
for timeline_path: String in DialogicResourceUtil.list_resources_of_type('.translation'):
for file: String in DialogicUtil.listdir(timeline_path.get_base_dir()):
file = timeline_path.get_base_dir().path_join(file)
if file.ends_with('.translation'):
if not file in translation_files:
translation_files.append(file)
if translation_mode == TranslationModes.PER_PROJECT:
var translation_folder: String = ProjectSettings.get_setting('dialogic/translation/translation_folder', 'res://')
for file: String in DialogicUtil.listdir(translation_folder):
file = translation_folder.path_join(file)
if file.ends_with('.translation'):
if not file in translation_files:
translation_files.append(file)
var all_translation_files: Array = ProjectSettings.get_setting('internationalization/locale/translations', [])
var orig_file_amount := len(all_translation_files)
# This array keeps track of valid translation file paths.
var found_file_paths := []
var removed_translation_files := 0
for file_path: String in translation_files:
# If the file path is not valid, we must clean it up.
if ResourceLoader.exists(file_path):
found_file_paths.append(file_path)
else:
removed_translation_files += 1
continue
if not file_path in all_translation_files:
all_translation_files.append(file_path)
var path_without_suffix := file_path.trim_suffix('.translation')
var locale_part := path_without_suffix.split(".")[-1]
_collect_locale(locale_part)
var valid_translation_files := PackedStringArray(all_translation_files)
ProjectSettings.set_setting('internationalization/locale/translations', valid_translation_files)
ProjectSettings.save()
%StatusMessage.text = (
"Added translation files: " + str(len(all_translation_files)-orig_file_amount)
+ "\nRemoved translation files: " + str(removed_translation_files)
+ "\nTotal translation files: " + str(len(all_translation_files)))
func _on_erase_translations_pressed() -> void:
%EraseConfirmationDialog.popup_centered()
## Deletes translation files generated by [param csv_name].
## The [param csv_name] may not contain the file extension (.csv).
##
## Returns a vector, value 1 is amount of deleted translation files.
## Value
func delete_translations_files(translation_files: Array, csv_name: String) -> int:
var deleted_files := 0
for file_path: String in DialogicResourceUtil.list_resources_of_type('.translation'):
var base_name: String = file_path.get_basename()
var path_parts := base_name.split("/")
var translation_name: String = path_parts[-1]
if translation_name.begins_with(csv_name):
if OK == DirAccess.remove_absolute(file_path):
var project_translation_file_index := translation_files.find(file_path)
if project_translation_file_index > -1:
translation_files.remove_at(project_translation_file_index)
deleted_files += 1
print_rich("[color=green]Deleted translation file: " + file_path + "[/color]")
else:
print_rich("[color=yellow]Failed to delete translation file: " + file_path + "[/color]")
return deleted_files
## Iterates over all timelines and deletes their CSVs and timeline
## translation IDs.
## Deletes the Per-Project CSV file and the character name CSV file.
func erase_translations() -> void:
var files: PackedStringArray = ProjectSettings.get_setting('internationalization/locale/translations', [])
var translation_files := Array(files)
ProjectSettings.set_setting(_USED_LOCALES_SETTING, [])
var deleted_csv_files := 0
var deleted_translation_files := 0
var cleaned_timelines := 0
var cleaned_characters := 0
var cleaned_events := 0
var cleaned_glossaries := 0
var current_timeline := _close_active_timeline()
# Delete all Dialogic CSV files and their translation files.
for csv_path: String in DialogicResourceUtil.list_resources_of_type(".csv"):
var csv_path_parts: PackedStringArray = csv_path.split("/")
var csv_name: String = csv_path_parts[-1].trim_suffix(".csv")
# Handle Dialogic CSVs only.
if not csv_name.begins_with("dialogic_"):
continue
# Delete the CSV file.
if OK == DirAccess.remove_absolute(csv_path):
deleted_csv_files += 1
print_rich("[color=green]Deleted CSV file: " + csv_path + "[/color]")
deleted_translation_files += delete_translations_files(translation_files, csv_name)
else:
print_rich("[color=yellow]Failed to delete CSV file: " + csv_path + "[/color]")
# Clean timelines.
for timeline_path: String in DialogicResourceUtil.list_resources_of_type(".dtl"):
# Process the timeline.
var timeline: DialogicTimeline = load(timeline_path)
timeline.process()
cleaned_timelines += 1
# Remove event translation IDs.
for event: DialogicEvent in timeline.events:
if event._translation_id and not event._translation_id.is_empty():
event.remove_translation_id()
event.update_text_version()
cleaned_events += 1
if "character" in event:
# Remove character translation IDs.
var character: DialogicCharacter = event.character
if character != null and not character._translation_id.is_empty():
character.remove_translation_id()
cleaned_characters += 1
timeline.set_meta("timeline_not_saved", true)
ResourceSaver.save(timeline, timeline_path)
_erase_glossary_translation_ids()
_erase_character_name_translation_ids()
ProjectSettings.set_setting('dialogic/translation/id_counter', 16)
ProjectSettings.set_setting('internationalization/locale/translations', PackedStringArray(translation_files))
ProjectSettings.save()
find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
var status_message := "Timelines cleaned {cleaned_timelines}
Events cleaned {cleaned_events}
Characters cleaned {cleaned_characters}
Glossaries cleaned {cleaned_glossaries}
CSVs erased {erased_csv_files}
Translations erased {erased_translation_files}"
var status_message_args := {
'cleaned_timelines': cleaned_timelines,
'cleaned_characters': cleaned_characters,
'cleaned_events': cleaned_events,
'cleaned_glossaries': cleaned_glossaries,
'erased_csv_files': deleted_csv_files,
'erased_translation_files': deleted_translation_files,
}
_silently_open_timeline(current_timeline)
# Trigger reimport.
find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
# Clear the internal settings.
ProjectSettings.clear('dialogic/translation/intern/save_mode')
ProjectSettings.clear('dialogic/translation/intern/file_mode')
ProjectSettings.clear('dialogic/translation/intern/translation_folder')
_verify_translation_file()
%StatusMessage.text = status_message.format(status_message_args)
func _erase_glossary_translation_ids() -> void:
# Clean glossary.
var glossary_paths: Array = ProjectSettings.get_setting('dialogic/glossary/glossary_files', [])
for glossary_path: String in glossary_paths:
var glossary: DialogicGlossary = load(glossary_path)
glossary.remove_translation_id()
glossary.remove_entry_translation_ids()
glossary.clear_translation_keys()
ResourceSaver.save(glossary, glossary_path)
print_rich("[color=green]Cleaned up glossary file: " + glossary_path + "[/color]")
func _erase_character_name_translation_ids() -> void:
for character_path: String in DialogicResourceUtil.list_resources_of_type('.dch'):
var character: DialogicCharacter = load(character_path)
character.remove_translation_id()
ResourceSaver.save(character)
## Closes the current timeline in the Dialogic Editor and returns the timeline
## as a resource.
## If no timeline has been opened, returns null.
func _close_active_timeline() -> Resource:
var timeline_node: DialogicEditor = settings_editor.editors_manager.editors['Timeline']['node']
# We will close this timeline to ensure it will properly update.
# By saving this reference, we can open it again.
var current_timeline := timeline_node.current_resource
# Clean the current editor, this will also close the timeline.
settings_editor.editors_manager.clear_editor(timeline_node)
return current_timeline
## Opens the timeline resource into the Dialogic Editor.
## If the timeline is null, does nothing.
func _silently_open_timeline(timeline_to_open: Resource) -> void:
if timeline_to_open != null:
settings_editor.editors_manager.edit_resource(timeline_to_open, true, true)
## Checks [param locale] for unique locales that have not been added
## to the [_unique_locales] array yet.
func _collect_locale(locale: String) -> void:
if _unique_locales.has(locale):
return
_unique_locales.append(locale)