2 extends DialogicSettingsPage
4 ## Settings tab that allows enabeling and updating translation csv-files.
7 enum TranslationModes {PER_PROJECT, PER_TIMELINE, NONE}
8 enum SaveLocationModes {INSIDE_TRANSLATION_FOLDER, NEXT_TO_TIMELINE, NONE}
11 @onready var settings_editor: Control = find_parent('Settings')
13 ## The default CSV filename that contains the translations for character
15 const DEFAULT_CHARACTER_CSV_NAME := "dialogic_character_translations.csv"
16 ## The default CSV filename that contains the translations for timelines.
17 ## Only used when all timelines are supposed to be translated in one file.
18 const DEFAULT_TIMELINE_CSV_NAME := "dialogic_timeline_translations.csv"
20 const DEFAULT_GLOSSARY_CSV_NAME := "dialogic_glossary_translations.csv"
22 const _USED_LOCALES_SETTING := "dialogic/translation/locales"
24 ## Contains translation changes that were made during the last update.
26 ## Unique locales that will be set after updating the CSV files.
27 var _unique_locales := []
29 func _get_icon() -> Texture2D:
30 return get_theme_icon("Translation", "EditorIcons")
33 func _is_feature_tab() -> bool:
37 func _ready() -> void:
38 %TransEnabled.toggled.connect(store_changes)
39 %OrigLocale.get_suggestions_func = get_locales
40 %OrigLocale.resource_icon = get_theme_icon("Translation", "EditorIcons")
41 %OrigLocale.value_changed.connect(store_changes)
42 %TestingLocale.get_suggestions_func = get_locales
43 %TestingLocale.resource_icon = get_theme_icon("Translation", "EditorIcons")
44 %TestingLocale.value_changed.connect(store_changes)
45 %TransFolderPicker.value_changed.connect(store_changes)
46 %AddSeparatorEnabled.toggled.connect(store_changes)
48 %SaveLocationMode.item_selected.connect(store_changes)
49 %TransMode.item_selected.connect(store_changes)
51 %UpdateCsvFiles.pressed.connect(_on_update_translations_pressed)
52 %UpdateCsvFiles.icon = get_theme_icon("Add", "EditorIcons")
54 %CollectTranslations.pressed.connect(collect_translations)
55 %CollectTranslations.icon = get_theme_icon("File", "EditorIcons")
57 %TransRemove.pressed.connect(_on_erase_translations_pressed)
58 %TransRemove.icon = get_theme_icon("Remove", "EditorIcons")
60 %UpdateConfirmationDialog.add_button("Keep old & Generate new", false, "generate_new")
62 %UpdateConfirmationDialog.custom_action.connect(_on_custom_action)
64 _verify_translation_file()
67 func _on_custom_action(action: String) -> void:
68 if action == "generate_new":
72 func _refresh() -> void:
75 %TransEnabled.button_pressed = ProjectSettings.get_setting('dialogic/translation/enabled', false)
76 %TranslationSettings.visible = %TransEnabled.button_pressed
77 %OrigLocale.set_value(ProjectSettings.get_setting('dialogic/translation/original_locale', TranslationServer.get_tool_locale()))
78 %TransMode.select(ProjectSettings.get_setting('dialogic/translation/file_mode', 1))
79 %TransFolderPicker.set_value(ProjectSettings.get_setting('dialogic/translation/translation_folder', ''))
80 %TestingLocale.set_value(ProjectSettings.get_setting('internationalization/locale/test', ''))
81 %AddSeparatorEnabled.button_pressed = ProjectSettings.get_setting('dialogic/translation/add_separator', false)
83 _verify_translation_file()
88 func store_changes(_fake_arg: Variant = null, _fake_arg2: Variant = null) -> void:
92 _verify_translation_file()
94 ProjectSettings.set_setting('dialogic/translation/enabled', %TransEnabled.button_pressed)
95 %TranslationSettings.visible = %TransEnabled.button_pressed
96 ProjectSettings.set_setting('dialogic/translation/original_locale', %OrigLocale.current_value)
97 ProjectSettings.set_setting('dialogic/translation/file_mode', %TransMode.selected)
98 ProjectSettings.set_setting('dialogic/translation/translation_folder', %TransFolderPicker.current_value)
99 ProjectSettings.set_setting('internationalization/locale/test', %TestingLocale.current_value)
100 ProjectSettings.set_setting('dialogic/translation/save_mode', %SaveLocationMode.selected)
101 ProjectSettings.set_setting('dialogic/translation/add_separator', %AddSeparatorEnabled.button_pressed)
102 ProjectSettings.save()
105 ## Checks whether the translation folder path is required.
106 ## If it is, disables the "Update CSV files" button and shows a warning.
108 ## The translation folder path is required when either of the following is true:
109 ## - The translation mode is set to "Per Project".
110 ## - The save location mode is set to "Inside Translation Folder".
111 func _verify_translation_file() -> void:
112 var translation_folder: String = %TransFolderPicker.current_value
113 var file_mode: TranslationModes = %TransMode.selected
115 if file_mode == TranslationModes.PER_PROJECT:
116 %SaveLocationMode.disabled = true
118 %SaveLocationMode.disabled = false
120 var valid_translation_folder := (!translation_folder.is_empty()
121 and DirAccess.dir_exists_absolute(translation_folder))
123 %UpdateCsvFiles.disabled = not valid_translation_folder
125 var status_message := ""
127 if not valid_translation_folder:
128 status_message += "⛔ Requires valid translation folder to translate character names"
130 if file_mode == TranslationModes.PER_PROJECT:
131 status_message += " and the project CSV file."
133 status_message += "."
135 %StatusMessage.text = status_message
138 func get_locales(_filter: String) -> Dictionary:
139 var suggestions := {}
140 suggestions['Default'] = {'value':'', 'tooltip':"Will use the fallback locale set in the project settings."}
141 suggestions[TranslationServer.get_tool_locale()] = {'value':TranslationServer.get_tool_locale()}
143 var used_locales: Array = ProjectSettings.get_setting(_USED_LOCALES_SETTING, TranslationServer.get_all_languages())
145 for locale: String in used_locales:
146 var language_name := TranslationServer.get_language_name(locale)
148 # Invalid locales return an empty String.
149 if language_name.is_empty():
152 suggestions[locale] = { 'value': locale, 'tooltip': language_name }
157 func _on_update_translations_pressed() -> void:
158 var save_mode: SaveLocationModes = %SaveLocationMode.selected
159 var file_mode: TranslationModes = %TransMode.selected
160 var translation_folder: String = %TransFolderPicker.current_value
162 var old_save_mode: SaveLocationModes = ProjectSettings.get_setting('dialogic/translation/intern/save_mode', save_mode)
163 var old_file_mode: TranslationModes = ProjectSettings.get_setting('dialogic/translation/intern/file_mode', file_mode)
164 var old_translation_folder: String = ProjectSettings.get_setting('dialogic/translation/intern/translation_folder', translation_folder)
166 if (old_save_mode == save_mode
167 and old_file_mode == file_mode
168 and old_translation_folder == translation_folder):
172 %UpdateConfirmationDialog.popup_centered()
175 ## Used by the dialog to inform that the settings were changed.
176 func _delete_and_update() -> void:
181 ## Creates or updates the glossary CSV files.
182 func _handle_glossary_translation(
183 csv_data: CsvUpdateData,
184 save_location_mode: SaveLocationModes,
185 translation_mode: TranslationModes,
186 translation_folder_path: String,
187 orig_locale: String) -> void:
189 var glossary_csv: DialogicCsvFile = null
190 var glossary_paths: Array = ProjectSettings.get_setting('dialogic/glossary/glossary_files', [])
191 var add_separator_lines: bool = ProjectSettings.get_setting('dialogic/translation/add_separator', false)
193 for glossary_path: String in glossary_paths:
195 if glossary_csv == null:
198 # Get glossary CSV file name.
199 match translation_mode:
200 TranslationModes.PER_PROJECT:
201 csv_name = DEFAULT_GLOSSARY_CSV_NAME
203 TranslationModes.PER_TIMELINE:
204 var glossary_name: String = glossary_path.trim_suffix('.tres')
205 var path_parts := glossary_name.split("/")
206 var file_name := path_parts[-1]
207 csv_name = "dialogic_" + file_name + '_translation.csv'
209 var glossary_csv_path := ""
210 # Get glossary CSV file path.
211 match save_location_mode:
212 SaveLocationModes.INSIDE_TRANSLATION_FOLDER:
213 glossary_csv_path = translation_folder_path.path_join(csv_name)
215 SaveLocationModes.NEXT_TO_TIMELINE:
216 glossary_csv_path = glossary_path.get_base_dir().path_join(csv_name)
218 # Create or update glossary CSV file.
219 glossary_csv = DialogicCsvFile.new(glossary_csv_path, orig_locale, add_separator_lines)
221 if (glossary_csv.is_new_file):
222 csv_data.new_glossaries += 1
224 csv_data.updated_glossaries += 1
226 var glossary: DialogicGlossary = load(glossary_path)
227 glossary_csv.collect_lines_from_glossary(glossary)
228 glossary_csv.add_translation_keys_to_glossary(glossary)
229 ResourceSaver.save(glossary)
231 #If per-file mode is used, save this csv and begin a new one
232 if translation_mode == TranslationModes.PER_TIMELINE:
233 glossary_csv.update_csv_file_on_disk()
236 # If a Per-Project glossary is still open, we need to save it.
237 if glossary_csv != null:
238 glossary_csv.update_csv_file_on_disk()
242 ## Keeps information about the amount of new and updated CSV rows and what
243 ## resources were populated with translation IDs.
244 ## The final data can be used to display a status message.
247 var updated_events := 0
249 var new_timelines := 0
250 var updated_timelines := 0
253 var updated_names := 0
255 var new_glossaries := 0
256 var updated_glossaries := 0
258 var new_glossary_entries := 0
259 var updated_glossary_entries := 0
262 func update_csv_files() -> void:
264 var orig_locale: String = ProjectSettings.get_setting('dialogic/translation/original_locale', '').strip_edges()
265 var save_location_mode: SaveLocationModes = ProjectSettings.get_setting('dialogic/translation/save_mode', SaveLocationModes.NEXT_TO_TIMELINE)
266 var translation_mode: TranslationModes = ProjectSettings.get_setting('dialogic/translation/file_mode', TranslationModes.PER_PROJECT)
267 var translation_folder_path: String = ProjectSettings.get_setting('dialogic/translation/translation_folder', 'res://')
268 var add_separator_lines: bool = ProjectSettings.get_setting('dialogic/translation/add_separator', false)
270 var csv_data := CsvUpdateData.new()
272 if orig_locale.is_empty():
273 orig_locale = ProjectSettings.get_setting('internationalization/locale/fallback')
275 ProjectSettings.set_setting('dialogic/translation/intern/save_mode', save_location_mode)
276 ProjectSettings.set_setting('dialogic/translation/intern/file_mode', translation_mode)
277 ProjectSettings.set_setting('dialogic/translation/intern/translation_folder', translation_folder_path)
279 var current_timeline := _close_active_timeline()
281 var csv_per_project: DialogicCsvFile = null
282 var per_project_csv_path := translation_folder_path.path_join(DEFAULT_TIMELINE_CSV_NAME)
284 if translation_mode == TranslationModes.PER_PROJECT:
285 csv_per_project = DialogicCsvFile.new(per_project_csv_path, orig_locale, add_separator_lines)
287 if (csv_per_project.is_new_file):
288 csv_data.new_timelines += 1
290 csv_data.updated_timelines += 1
292 # Iterate over all timelines.
293 # Create or update CSV files.
294 # Transform the timeline into translatable lines and collect into the CSV file.
295 for timeline_path: String in DialogicResourceUtil.list_resources_of_type('.dtl'):
296 var csv_file: DialogicCsvFile = csv_per_project
298 # Swap the CSV file to the Per Timeline one.
299 if translation_mode == TranslationModes.PER_TIMELINE:
300 var per_timeline_path: String = timeline_path.trim_suffix('.dtl')
301 var path_parts := per_timeline_path.split("/")
302 var timeline_name: String = path_parts[-1]
304 # Adjust the file path to the translation location mode.
305 if save_location_mode == SaveLocationModes.INSIDE_TRANSLATION_FOLDER:
306 var prefixed_timeline_name := "dialogic_" + timeline_name
307 per_timeline_path = translation_folder_path.path_join(prefixed_timeline_name)
310 per_timeline_path += '_translation.csv'
311 csv_file = DialogicCsvFile.new(per_timeline_path, orig_locale, false)
312 csv_data.new_timelines += 1
314 # Load and process timeline, turn events into resources.
315 var timeline: DialogicTimeline = load(timeline_path)
317 if timeline.events.size() == 0:
318 print_rich("[color=yellow]Empty timeline, skipping: " + timeline_path + "[/color]")
323 # Collect timeline into CSV.
324 csv_file.collect_lines_from_timeline(timeline)
326 # in case new translation_id's were added, we save the timeline again
327 timeline.set_meta("timeline_not_saved", true)
328 ResourceSaver.save(timeline, timeline_path)
330 if translation_mode == TranslationModes.PER_TIMELINE:
331 csv_file.update_csv_file_on_disk()
333 csv_data.new_events += csv_file.new_rows
334 csv_data.updated_events += csv_file.updated_rows
336 _handle_glossary_translation(
340 translation_folder_path,
344 _handle_character_names(
347 translation_folder_path,
351 if translation_mode == TranslationModes.PER_PROJECT:
352 csv_per_project.update_csv_file_on_disk()
354 _silently_open_timeline(current_timeline)
357 find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
359 var status_message := "Events created {new_events} found {updated_events}
360 Names created {new_names} found {updated_names}
361 CSVs created {new_timelines} found {updated_timelines}
362 Glossary created {new_glossaries} found {updated_glossaries}
363 Entries created {new_glossary_entries} found {updated_glossary_entries}"
365 var status_message_args := {
366 'new_events': csv_data.new_events,
367 'updated_events': csv_data.updated_events,
368 'new_timelines': csv_data.new_timelines,
369 'updated_timelines': csv_data.updated_timelines,
370 'new_glossaries': csv_data.new_glossaries,
371 'updated_glossaries': csv_data.updated_glossaries,
372 'new_names': csv_data.new_names,
373 'updated_names': csv_data.updated_names,
374 'new_glossary_entries': csv_data.new_glossary_entries,
375 'updated_glossary_entries': csv_data.updated_glossary_entries,
378 %StatusMessage.text = status_message.format(status_message_args)
379 ProjectSettings.set_setting(_USED_LOCALES_SETTING, _unique_locales)
382 ## Iterates over all character resource files and creates or updates CSV files
383 ## that contain the translations for character properties.
384 ## This will save each character resource file to disk.
385 func _handle_character_names(
386 csv_data: CsvUpdateData,
387 original_locale: String,
388 translation_folder_path: String,
389 add_separator_lines: bool) -> void:
390 var names_csv_path := translation_folder_path.path_join(DEFAULT_CHARACTER_CSV_NAME)
391 var character_name_csv: DialogicCsvFile = DialogicCsvFile.new(names_csv_path,
396 var all_characters := {}
398 for character_path: String in DialogicResourceUtil.list_resources_of_type('.dch'):
399 var character: DialogicCharacter = load(character_path)
401 if character._translation_id.is_empty():
402 csv_data.new_names += 1
405 csv_data.updated_names += 1
407 var translation_id := character.get_set_translation_id()
408 all_characters[translation_id] = character
410 ResourceSaver.save(character)
412 character_name_csv.collect_lines_from_characters(all_characters)
413 character_name_csv.update_csv_file_on_disk()
416 func collect_translations() -> void:
417 var translation_files := []
418 var translation_mode: TranslationModes = ProjectSettings.get_setting('dialogic/translation/file_mode', TranslationModes.PER_PROJECT)
420 if translation_mode == TranslationModes.PER_TIMELINE:
422 for timeline_path: String in DialogicResourceUtil.list_resources_of_type('.translation'):
424 for file: String in DialogicUtil.listdir(timeline_path.get_base_dir()):
425 file = timeline_path.get_base_dir().path_join(file)
427 if file.ends_with('.translation'):
429 if not file in translation_files:
430 translation_files.append(file)
432 if translation_mode == TranslationModes.PER_PROJECT:
433 var translation_folder: String = ProjectSettings.get_setting('dialogic/translation/translation_folder', 'res://')
435 for file: String in DialogicUtil.listdir(translation_folder):
436 file = translation_folder.path_join(file)
438 if file.ends_with('.translation'):
440 if not file in translation_files:
441 translation_files.append(file)
443 var all_translation_files: Array = ProjectSettings.get_setting('internationalization/locale/translations', [])
444 var orig_file_amount := len(all_translation_files)
446 # This array keeps track of valid translation file paths.
447 var found_file_paths := []
448 var removed_translation_files := 0
450 for file_path: String in translation_files:
451 # If the file path is not valid, we must clean it up.
452 if ResourceLoader.exists(file_path):
453 found_file_paths.append(file_path)
455 removed_translation_files += 1
458 if not file_path in all_translation_files:
459 all_translation_files.append(file_path)
461 var path_without_suffix := file_path.trim_suffix('.translation')
462 var locale_part := path_without_suffix.split(".")[-1]
463 _collect_locale(locale_part)
466 var valid_translation_files := PackedStringArray(all_translation_files)
467 ProjectSettings.set_setting('internationalization/locale/translations', valid_translation_files)
468 ProjectSettings.save()
470 %StatusMessage.text = (
471 "Added translation files: " + str(len(all_translation_files)-orig_file_amount)
472 + "\nRemoved translation files: " + str(removed_translation_files)
473 + "\nTotal translation files: " + str(len(all_translation_files)))
476 func _on_erase_translations_pressed() -> void:
477 %EraseConfirmationDialog.popup_centered()
480 ## Deletes translation files generated by [param csv_name].
481 ## The [param csv_name] may not contain the file extension (.csv).
483 ## Returns a vector, value 1 is amount of deleted translation files.
485 func delete_translations_files(translation_files: Array, csv_name: String) -> int:
486 var deleted_files := 0
488 for file_path: String in DialogicResourceUtil.list_resources_of_type('.translation'):
489 var base_name: String = file_path.get_basename()
490 var path_parts := base_name.split("/")
491 var translation_name: String = path_parts[-1]
493 if translation_name.begins_with(csv_name):
495 if OK == DirAccess.remove_absolute(file_path):
496 var project_translation_file_index := translation_files.find(file_path)
498 if project_translation_file_index > -1:
499 translation_files.remove_at(project_translation_file_index)
502 print_rich("[color=green]Deleted translation file: " + file_path + "[/color]")
504 print_rich("[color=yellow]Failed to delete translation file: " + file_path + "[/color]")
510 ## Iterates over all timelines and deletes their CSVs and timeline
512 ## Deletes the Per-Project CSV file and the character name CSV file.
513 func erase_translations() -> void:
514 var files: PackedStringArray = ProjectSettings.get_setting('internationalization/locale/translations', [])
515 var translation_files := Array(files)
516 ProjectSettings.set_setting(_USED_LOCALES_SETTING, [])
518 var deleted_csv_files := 0
519 var deleted_translation_files := 0
520 var cleaned_timelines := 0
521 var cleaned_characters := 0
522 var cleaned_events := 0
523 var cleaned_glossaries := 0
525 var current_timeline := _close_active_timeline()
527 # Delete all Dialogic CSV files and their translation files.
528 for csv_path: String in DialogicResourceUtil.list_resources_of_type(".csv"):
529 var csv_path_parts: PackedStringArray = csv_path.split("/")
530 var csv_name: String = csv_path_parts[-1].trim_suffix(".csv")
532 # Handle Dialogic CSVs only.
533 if not csv_name.begins_with("dialogic_"):
536 # Delete the CSV file.
537 if OK == DirAccess.remove_absolute(csv_path):
538 deleted_csv_files += 1
539 print_rich("[color=green]Deleted CSV file: " + csv_path + "[/color]")
541 deleted_translation_files += delete_translations_files(translation_files, csv_name)
543 print_rich("[color=yellow]Failed to delete CSV file: " + csv_path + "[/color]")
546 for timeline_path: String in DialogicResourceUtil.list_resources_of_type(".dtl"):
548 # Process the timeline.
549 var timeline: DialogicTimeline = load(timeline_path)
551 cleaned_timelines += 1
553 # Remove event translation IDs.
554 for event: DialogicEvent in timeline.events:
556 if event._translation_id and not event._translation_id.is_empty():
557 event.remove_translation_id()
558 event.update_text_version()
561 if "character" in event:
562 # Remove character translation IDs.
563 var character: DialogicCharacter = event.character
565 if character != null and not character._translation_id.is_empty():
566 character.remove_translation_id()
567 cleaned_characters += 1
569 timeline.set_meta("timeline_not_saved", true)
570 ResourceSaver.save(timeline, timeline_path)
572 _erase_glossary_translation_ids()
573 _erase_character_name_translation_ids()
575 ProjectSettings.set_setting('dialogic/translation/id_counter', 16)
576 ProjectSettings.set_setting('internationalization/locale/translations', PackedStringArray(translation_files))
577 ProjectSettings.save()
579 find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
581 var status_message := "Timelines cleaned {cleaned_timelines}
582 Events cleaned {cleaned_events}
583 Characters cleaned {cleaned_characters}
584 Glossaries cleaned {cleaned_glossaries}
586 CSVs erased {erased_csv_files}
587 Translations erased {erased_translation_files}"
589 var status_message_args := {
590 'cleaned_timelines': cleaned_timelines,
591 'cleaned_characters': cleaned_characters,
592 'cleaned_events': cleaned_events,
593 'cleaned_glossaries': cleaned_glossaries,
594 'erased_csv_files': deleted_csv_files,
595 'erased_translation_files': deleted_translation_files,
598 _silently_open_timeline(current_timeline)
601 find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources()
603 # Clear the internal settings.
604 ProjectSettings.clear('dialogic/translation/intern/save_mode')
605 ProjectSettings.clear('dialogic/translation/intern/file_mode')
606 ProjectSettings.clear('dialogic/translation/intern/translation_folder')
608 _verify_translation_file()
609 %StatusMessage.text = status_message.format(status_message_args)
612 func _erase_glossary_translation_ids() -> void:
614 var glossary_paths: Array = ProjectSettings.get_setting('dialogic/glossary/glossary_files', [])
616 for glossary_path: String in glossary_paths:
617 var glossary: DialogicGlossary = load(glossary_path)
618 glossary.remove_translation_id()
619 glossary.remove_entry_translation_ids()
620 glossary.clear_translation_keys()
621 ResourceSaver.save(glossary, glossary_path)
622 print_rich("[color=green]Cleaned up glossary file: " + glossary_path + "[/color]")
625 func _erase_character_name_translation_ids() -> void:
626 for character_path: String in DialogicResourceUtil.list_resources_of_type('.dch'):
627 var character: DialogicCharacter = load(character_path)
629 character.remove_translation_id()
630 ResourceSaver.save(character)
633 ## Closes the current timeline in the Dialogic Editor and returns the timeline
635 ## If no timeline has been opened, returns null.
636 func _close_active_timeline() -> Resource:
637 var timeline_node: DialogicEditor = settings_editor.editors_manager.editors['Timeline']['node']
638 # We will close this timeline to ensure it will properly update.
639 # By saving this reference, we can open it again.
640 var current_timeline := timeline_node.current_resource
641 # Clean the current editor, this will also close the timeline.
642 settings_editor.editors_manager.clear_editor(timeline_node)
644 return current_timeline
647 ## Opens the timeline resource into the Dialogic Editor.
648 ## If the timeline is null, does nothing.
649 func _silently_open_timeline(timeline_to_open: Resource) -> void:
650 if timeline_to_open != null:
651 settings_editor.editors_manager.edit_resource(timeline_to_open, true, true)
654 ## Checks [param locale] for unique locales that have not been added
655 ## to the [_unique_locales] array yet.
656 func _collect_locale(locale: String) -> void:
657 if _unique_locales.has(locale):
660 _unique_locales.append(locale)