From d280bed6a1afd16114cabdecc39641ba56e07417 Mon Sep 17 00:00:00 2001 From: Cake <65981767+CakeVR@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:33:18 +0100 Subject: [PATCH] Add Character Name Translation (#1898) * Generate translation for character names. * Add property translation to `DialogicCharacter`. * Fix property key. * Improve counting CSV updates and additions. * Improve `Update CSV files` status message. * Add check if `current_resource` is not `null`. * Fix file paths for characters and `Per Timeline` mode. * Refactor CSV and translation deletion. * Fix using wrong default name for character CSV. * Lock Translation Settings if CSV were generated. * Improve deletion of CSV related items. * Hide dialogue window. * Improve Translation Settings text. * Add info about deleting translation IDs. * Use translated character nicknames. * Add missing type. * Remove unused variable. * Add missing newlines. * Remove redundant `await`. * Add `get_display_name_translated`. * Check if event has `character`. * Add type Godot cannot infer. * Fix `characters` variable exist check. * Update method usage. * Check if translation is translation key. * Fix check if translation is translation key. * Fix duplicates of character properties in the CSV. They duplicated if there were multiple timelines referencing characters. * Fix word. * Fix handling timeline CSV collecting. * Small fixes for character line collection * Fix two translation bugs - Fixed translated nicknames method complaining about PackedStringArray (godot being stupid) - Fixed event translations being compared to incorrect key for fallback. * Fix closing and opening editor timeline on deletion. --------- Co-authored-by: Jowan-Spooner --- addons/dialogic/Editor/Common/sidebar.gd | 14 +- .../Editor/Events/Fields/ComplexPicker.gd | 4 + .../Editor/Events/Fields/FilePicker.gd | 11 +- addons/dialogic/Editor/Settings/csv_file.gd | 185 ++++++--- .../Editor/Settings/settings_general.tscn | 22 +- .../Editor/Settings/settings_translation.gd | 357 +++++++++++++----- .../Editor/Settings/settings_translation.tscn | 131 ++++--- addons/dialogic/Editor/editors_manager.gd | 2 +- .../dialogic/Modules/Text/subsystem_text.gd | 17 +- addons/dialogic/Resources/character.gd | 88 ++++- addons/dialogic/Resources/event.gd | 2 +- 11 files changed, 612 insertions(+), 221 deletions(-) diff --git a/addons/dialogic/Editor/Common/sidebar.gd b/addons/dialogic/Editor/Common/sidebar.gd index d1badb415..56584948f 100644 --- a/addons/dialogic/Editor/Common/sidebar.gd +++ b/addons/dialogic/Editor/Common/sidebar.gd @@ -151,9 +151,17 @@ func update_content_list(list:PackedStringArray) -> void: if list.is_empty(): return - for i in editors_manager.resource_helper.timeline_directory: - if editors_manager.resource_helper.timeline_directory[i] == editors_manager.get_current_editor().current_resource.resource_path: - editors_manager.resource_helper.label_directory[i] = list + var current_resource: Resource = editors_manager.get_current_editor().current_resource + + if current_resource != null: + var current_resource_path := current_resource.resource_path + + for i in editors_manager.resource_helper.timeline_directory: + + if editors_manager.resource_helper.timeline_directory[i] == current_resource_path: + editors_manager.resource_helper.label_directory[i] = list + + editors_manager.resource_helper.label_directory[''] = list DialogicUtil.set_editor_setting('label_ref', editors_manager.resource_helper.label_directory) diff --git a/addons/dialogic/Editor/Events/Fields/ComplexPicker.gd b/addons/dialogic/Editor/Events/Fields/ComplexPicker.gd index 8d50fa8dd..5bd8bd4fa 100644 --- a/addons/dialogic/Editor/Events/Fields/ComplexPicker.gd +++ b/addons/dialogic/Editor/Events/Fields/ComplexPicker.gd @@ -92,6 +92,10 @@ func _exit_tree(): func take_autofocus(): %Search.grab_focus() + +func set_enabled(is_enabled: bool) -> void: + %SelectButton.disabled = !is_enabled + ################################################################################ ## SEARCH & SUGGESTION POPUP ################################################################################ diff --git a/addons/dialogic/Editor/Events/Fields/FilePicker.gd b/addons/dialogic/Editor/Events/Fields/FilePicker.gd index c47f1e222..5c6599847 100644 --- a/addons/dialogic/Editor/Events/Fields/FilePicker.gd +++ b/addons/dialogic/Editor/Events/Fields/FilePicker.gd @@ -48,11 +48,16 @@ func set_value(value:String) -> void: else: %Field.custom_minimum_size.x = 0 %Field.expand_to_text_length = true - + %Field.text = text - + %ClearButton.visible = !value.is_empty() and !hide_reset - + + +func set_enabled(is_enabled: bool) -> void: + %Field.editable = is_enabled + %OpenButton.disabled = !is_enabled + %ClearButton.disabled = !is_enabled func _on_OpenButton_pressed() -> void: diff --git a/addons/dialogic/Editor/Settings/csv_file.gd b/addons/dialogic/Editor/Settings/csv_file.gd index 3029a28b0..411d52057 100644 --- a/addons/dialogic/Editor/Settings/csv_file.gd +++ b/addons/dialogic/Editor/Settings/csv_file.gd @@ -22,65 +22,130 @@ var file: FileAccess var used_file_path: String ## The amount of events that were updated in the CSV file. -var updated_events: int = 0 +var updated_rows: int = 0 ## The amount of events that were added to the CSV file. -var new_events: int = 0 +var new_rows: int = 0 + + +## Stores all character names from the current CSV. +## +## If this is CSV file for timeline events, every appearing speaker will be +## added once to this dictionary by their translation ID. +## If the translation ID does not exist, a new one will be generated. +## +## If this is the character name CSV file, this field captures all characters +## that were added to [member lines] using the +## [method collect_lines_from_characters]. +## +## Key: String, Value: PackedStringArray +var collected_characters: Dictionary = {} ## Attempts to load the CSV file from [param file_path]. ## If the file does not exist, a single entry is added to the [member lines] ## array. func _init(file_path: String, original_locale: String) -> void: - used_file_path = file_path + used_file_path = file_path - # The first entry must be the locale row. - # [method collect_lines_from_timeline] will add the other locales, if any. - var locale_array_line := PackedStringArray(["keys", original_locale]) - lines.append(locale_array_line) + # The first entry must be the locale row. + # [method collect_lines_from_timeline] will add the other locales, if any. + var locale_array_line := PackedStringArray(["keys", original_locale]) + lines.append(locale_array_line) - if not FileAccess.file_exists(file_path): - is_new_file = true + if not FileAccess.file_exists(file_path): + is_new_file = true - # The "keys" and original locale are the only columns in a new file. - # For example: "keys, en" - column_count = 2 + # The "keys" and original locale are the only columns in a new file. + # For example: "keys, en" + column_count = 2 - file = FileAccess.open(file_path, FileAccess.WRITE) - return + file = FileAccess.open(file_path, FileAccess.WRITE) + return - file = FileAccess.open(file_path, FileAccess.READ) + file = FileAccess.open(file_path, FileAccess.READ) - var locale_csv_row := file.get_csv_line() - column_count = locale_csv_row.size() - var locale_key := locale_csv_row[0] + var locale_csv_row := file.get_csv_line() + column_count = locale_csv_row.size() + var locale_key := locale_csv_row[0] - old_lines[locale_key] = locale_csv_row + old_lines[locale_key] = locale_csv_row - _read_file_into_lines() + _read_file_into_lines() ## Private function to read the CSV file into the [member lines] array. func _read_file_into_lines() -> void: - while not file.eof_reached(): - var line := file.get_csv_line() - var row_key := line[0] - old_lines[row_key] = line + while not file.eof_reached(): + var line := file.get_csv_line() + var row_key := line[0] + old_lines[row_key] = line + + +## Collects names from the given [param characters] and adds them to the +## [member lines]. +## +## If this is the character name CSV file, use this method to +## take previously collected characters from other [class DialogicCsvFile]s. +func collect_lines_from_characters(characters: Dictionary) -> void: + for character in characters.values(): + + # Check if the character has a valid translation ID. + if character._translation_id == null or character._translation_id.is_empty(): + character.add_translation_id() + + if character._translation_id in collected_characters: + continue + else: + collected_characters[character._translation_id] = character + + # Add row for display names. + var name_property := DialogicCharacter.TranslatedProperties.NAME + var display_name_key: String = character.get_property_translation_key(name_property) + var line_value: String = character.display_name + var array_line := PackedStringArray([display_name_key, line_value]) + lines.append(array_line) + + var character_nicknames: Array = character.nicknames + if character_nicknames.is_empty() or (character_nicknames.size() == 1 and character_nicknames[0].is_empty()): + return + + # Add row for nicknames. + var nick_name_property := DialogicCharacter.TranslatedProperties.NICKNAMES + var nickname_string: String = ", ".join(character_nicknames) + var nickname_name_line_key: String = character.get_property_translation_key(nick_name_property) + var nick_array_line := PackedStringArray([nickname_name_line_key, nickname_string]) + lines.append(nick_array_line) + + +## Collects translatable events from the given [param timeline] and adds +## them to the [member lines]. +## +## If this is a timeline CSV file, +func collect_lines_from_timeline(timeline: DialogicTimeline) -> void: + for event in timeline.events: + if event.can_be_translated(): -func collect_lines_from_timeline(timeline: DialogicTimeline) -> void: - for event in timeline.events: + if event._translation_id.is_empty(): + event.add_translation_id() + event.update_text_version() + + var properties: Array = event._get_translatable_properties() + + for property in properties: + var line_key: String = event.get_property_translation_key(property) + var line_value: String = event._get_property_original_translation(property) + var array_line := PackedStringArray([line_key, line_value]) + lines.append(array_line) - if event.can_be_translated(): + if not "character" in event: + continue - if event._translation_id.is_empty(): - event.add_translation_id() - event.update_text_version() + var character: DialogicCharacter = event.character - for property in event._get_translatable_properties(): - var line_key: String = event.get_property_translation_key(property) - var line_value: String = event._get_property_original_translation(property) - var array_line := PackedStringArray([line_key, line_value]) - lines.append(array_line) + if (character != null + and not collected_characters.has(character._translation_id)): + collected_characters[character._translation_id] = character ## Clears the CSV file on disk and writes the current [member lines] array to it. @@ -88,37 +153,37 @@ func collect_lines_from_timeline(timeline: DialogicTimeline) -> void: ## If a translation row misses a column, a trailing comma will be added to ## conform to the CSV file format. func update_csv_file_on_disk() -> void: - # Clear the current CSV file. - file = FileAccess.open(used_file_path, FileAccess.WRITE) + # Clear the current CSV file. + file = FileAccess.open(used_file_path, FileAccess.WRITE) - for line in lines: - var row_key := line[0] + for line in lines: + var row_key := line[0] - # In case there might be translations for this line already, - # add them at the end again (orig locale text is replaced). - if row_key in old_lines: - var old_line: PackedStringArray = old_lines[row_key] - var updated_line: PackedStringArray = line + old_line.slice(2) + # In case there might be translations for this line already, + # add them at the end again (orig locale text is replaced). + if row_key in old_lines: + var old_line: PackedStringArray = old_lines[row_key] + var updated_line: PackedStringArray = line + old_line.slice(2) - var line_columns: int = updated_line.size() - var line_columns_to_add := column_count - line_columns + var line_columns: int = updated_line.size() + var line_columns_to_add := column_count - line_columns - # Add trailing commas to match the amount of columns. - for _i in range(line_columns_to_add): - updated_line.append("") + # Add trailing commas to match the amount of columns. + for _i in range(line_columns_to_add): + updated_line.append("") - file.store_csv_line(updated_line) - updated_events += 1 + file.store_csv_line(updated_line) + updated_rows += 1 - else: - var line_columns: int = line.size() - var line_columns_to_add := column_count - line_columns + else: + var line_columns: int = line.size() + var line_columns_to_add := column_count - line_columns - # Add trailing commas to match the amount of columns. - for _i in range(line_columns_to_add): - line.append("") + # Add trailing commas to match the amount of columns. + for _i in range(line_columns_to_add): + line.append("") - file.store_csv_line(line) - new_events += 1 + file.store_csv_line(line) + new_rows += 1 - file.close() + file.close() diff --git a/addons/dialogic/Editor/Settings/settings_general.tscn b/addons/dialogic/Editor/Settings/settings_general.tscn index fb24630c9..51f73205a 100644 --- a/addons/dialogic/Editor/Settings/settings_general.tscn +++ b/addons/dialogic/Editor/Settings/settings_general.tscn @@ -4,7 +4,7 @@ [ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_kqhx5"] [ext_resource type="PackedScene" uid="uid://7mvxuaulctcq" path="res://addons/dialogic/Editor/Events/Fields/FilePicker.tscn" id="3_i7rug"] -[sub_resource type="Image" id="Image_pqmjp"] +[sub_resource type="Image" id="Image_e2a0f"] data = { "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", @@ -13,8 +13,8 @@ data = { "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_fyskv"] -image = SubResource("Image_pqmjp") +[sub_resource type="ImageTexture" id="ImageTexture_wyypx"] +image = SubResource("Image_e2a0f") [node name="General" type="VBoxContainer"] anchors_preset = 15 @@ -35,7 +35,7 @@ text = "Color Palette" [node name="HintTooltip" parent="PaletteTitle" instance=ExtResource("2_kqhx5")] layout_mode = 2 tooltip_text = "These colors are used for the events." -texture = SubResource("ImageTexture_fyskv") +texture = SubResource("ImageTexture_wyypx") hint_text = "These colors are used for the events." [node name="ResetColorsButton" type="Button" parent="PaletteTitle"] @@ -43,7 +43,7 @@ unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 0 tooltip_text = "Reset Colors to default" -icon = SubResource("ImageTexture_fyskv") +icon = SubResource("ImageTexture_wyypx") flat = true [node name="ScrollContainer" type="ScrollContainer" parent="."] @@ -72,7 +72,7 @@ tooltip_text = "The layout scene configured in the Layout editor is automaticall instanced when calling Dialogic.start(). Depending on your game, you might want it to be deleted after the dialogue, be hidden (as reinstancing often is wasting resources) or kept visible. " -texture = SubResource("ImageTexture_fyskv") +texture = SubResource("ImageTexture_wyypx") hint_text = "The layout scene configured in the Layout editor is automatically instanced when calling Dialogic.start(). Depending on your game, you might want it to be deleted after the dialogue, be hidden @@ -121,7 +121,7 @@ layout_mode = 2 tooltip_text = "Configure where dialogic looks for custom modules. You will have to restart the project to see the change take action." -texture = SubResource("ImageTexture_fyskv") +texture = SubResource("ImageTexture_wyypx") hint_text = "Configure where dialogic looks for custom modules. You will have to restart the project to see the change take action." @@ -139,7 +139,7 @@ layout_mode = 2 size_flags_horizontal = 3 placeholder = "res://addons/dialogic_additions/Events" file_mode = 2 -resource_icon = SubResource("ImageTexture_fyskv") +resource_icon = SubResource("ImageTexture_wyypx") [node name="VSeparator" type="VSeparator" parent="HBoxContainer6"] layout_mode = 2 @@ -164,7 +164,7 @@ text = "Extension Creator " [node name="HintTooltip" parent="HBoxContainer6/ExtensionsPanel/VBox/HBoxContainer6" instance=ExtResource("2_kqhx5")] layout_mode = 2 tooltip_text = "Use the Exension Creator to quickly setup custom modules!" -texture = SubResource("ImageTexture_fyskv") +texture = SubResource("ImageTexture_wyypx") hint_text = "Use the Exension Creator to quickly setup custom modules!" [node name="CreateExtensionButton" type="Button" parent="HBoxContainer6/ExtensionsPanel/VBox"] @@ -228,7 +228,7 @@ text = "Timer processing" [node name="HintTooltip" parent="HBoxContainer7" instance=ExtResource("2_kqhx5")] layout_mode = 2 tooltip_text = "Change whether dialogics timers process in physics_process (frame-rate independent) or process." -texture = SubResource("ImageTexture_fyskv") +texture = SubResource("ImageTexture_wyypx") hint_text = "Change whether dialogics timers process in physics_process (frame-rate independent) or process." [node name="HBoxContainer4" type="HBoxContainer" parent="."] @@ -256,7 +256,7 @@ text = "Section Order" [node name="HintTooltip" parent="HBoxContainer" instance=ExtResource("2_kqhx5")] layout_mode = 2 tooltip_text = "You can change the order of the event sections here. " -texture = SubResource("ImageTexture_fyskv") +texture = SubResource("ImageTexture_wyypx") hint_text = "You can change the order of the event sections here. " [node name="SectionList" type="Tree" parent="."] diff --git a/addons/dialogic/Editor/Settings/settings_translation.gd b/addons/dialogic/Editor/Settings/settings_translation.gd index cf1780094..929e4da82 100644 --- a/addons/dialogic/Editor/Settings/settings_translation.gd +++ b/addons/dialogic/Editor/Settings/settings_translation.gd @@ -4,12 +4,19 @@ extends DialogicSettingsPage ## Settings tab that allows enabeling and updating translation csv-files. -enum TranslationModes {PER_PROJECT, PER_TIMELINE} -enum SaveLocationModes {INSIDE_TRANSLATION_FOLDER, NEXT_TO_TIMELINE} +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" + func _get_icon(): return get_theme_icon("Translation", "EditorIcons") @@ -34,12 +41,27 @@ func _ready() -> void: %SaveLocationMode.item_selected.connect(store_changes) %TransMode.item_selected.connect(store_changes) - %UpdateCsvFiles.pressed.connect(update_csv_files) + %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 @@ -67,7 +89,7 @@ func store_changes(fake_arg = "", fake_arg2 = "") -> void: 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('internationalization/save_mode', %SaveLocationMode.selected) + ProjectSettings.set_setting('dialogic/translation/save_mode', %SaveLocationMode.selected) ProjectSettings.save() ## Checks whether the translation folder path is required. @@ -78,7 +100,6 @@ func store_changes(fake_arg = "", fake_arg2 = "") -> void: ## - The save location mode is set to "Inside Translation Folder". func _verify_translation_file() -> void: var translation_folder: String = %TransFolderPicker.current_value - var save_location_mode: SaveLocationModes = %SaveLocationMode.selected var file_mode: TranslationModes = %TransMode.selected if file_mode == TranslationModes.PER_PROJECT: @@ -86,20 +107,23 @@ func _verify_translation_file() -> void: else: %SaveLocationMode.disabled = false - if (save_location_mode == SaveLocationModes.INSIDE_TRANSLATION_FOLDER - or file_mode == TranslationModes.PER_PROJECT): - var valid_translation_folder = (!translation_folder.is_empty() - and DirAccess.dir_exists_absolute(translation_folder)) + var valid_translation_folder := (!translation_folder.is_empty() + and DirAccess.dir_exists_absolute(translation_folder)) + + %UpdateCsvFiles.disabled = not valid_translation_folder - %UpdateCsvFiles.disabled = !valid_translation_folder + var status_message := "" - if not valid_translation_folder: - %StatusMessage.text = "Invalid translation folder!" + 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: - %StatusMessage.text = "" + status_message += "." + + %StatusMessage.text = status_message - else: - %StatusMessage.text = "" func get_locales(filter:String) -> Dictionary: var suggestions := {} @@ -110,95 +134,144 @@ func get_locales(filter:String) -> Dictionary: 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() + + func update_csv_files() -> void: var orig_locale: String = ProjectSettings.get_setting('dialogic/translation/original_locale', '').strip_edges() - var save_location_mode: SaveLocationModes = ProjectSettings.get_setting('internationalization/save_mode', SaveLocationModes.NEXT_TO_TIMELINE) + 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://') if orig_locale.is_empty(): orig_locale = ProjectSettings.get_setting('internationalization/locale/fallback') - var translation_mode: TranslationModes = ProjectSettings.get_setting('dialogic/translation/file_mode', TranslationModes.PER_PROJECT) + 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) - # [new events, new_timelines, updated_events, updated_timelines] var new_events := 0 var new_timelines := 0 var updated_events := 0 var updated_timelines := 0 + var new_names := 0 + var updated_names := 0 - 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) + 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) - # Collect old lines from the Per Project CSV. if translation_mode == TranslationModes.PER_PROJECT: - var file_path: String = ProjectSettings.get_setting('dialogic/translation/translation_folder', 'res://').path_join('dialogic_translations.csv') - - csv_per_project = DialogicCsvFile.new(file_path, orig_locale) + csv_per_project = DialogicCsvFile.new(per_project_csv_path, orig_locale) if (csv_per_project.is_new_file): new_timelines += 1 else: updated_timelines += 1 + var names_csv_path := translation_folder_path.path_join(DEFAULT_CHARACTER_CSV_NAME) + var character_name_csv: DialogicCsvFile = DialogicCsvFile.new(names_csv_path, orig_locale) + + if (character_name_csv.is_new_file): + new_timelines += 1 + else: + updated_timelines += 1 + # Iterate over all timelines. - # Swap CSV file. + # Create or update CSV files. # Transform the timeline into translatable lines and collect into the CSV file. for timeline_path in DialogicUtil.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 file_path: String = timeline_path.trim_suffix('.dtl') + 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 path_parts := file_path.split("/") - var timeline_name: String = path_parts[-1] - var translation_folder: String = ProjectSettings.get_setting('dialogic/translation/translation_folder', 'res://') - - file_path = translation_folder.path_join(timeline_name) + var prefixed_timeline_name := "dialogic_" + timeline_name + per_timeline_path = translation_folder_path.path_join(prefixed_timeline_name) - file_path += '_translation.csv' - csv_file = DialogicCsvFile.new(file_path, orig_locale) - if csv_file.is_new_file: - new_timelines += 1 - else: - updated_timelines += 1 + per_timeline_path += '_translation.csv' + csv_file = DialogicCsvFile.new(per_timeline_path, orig_locale) + new_timelines += 1 # Load and process timeline, turn events into resources. var timeline: DialogicTimeline = load(timeline_path) - await timeline.process() + timeline.process() # Collect timeline into CSV. csv_file.collect_lines_from_timeline(timeline) + var characters := csv_file.collected_characters + character_name_csv.collect_lines_from_characters(characters) # 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) - csv_file.update_csv_file_on_disk() + if translation_mode == TranslationModes.PER_TIMELINE: + csv_file.update_csv_file_on_disk() + + new_events += csv_file.new_rows + updated_events += csv_file.updated_rows + + character_name_csv.update_csv_file_on_disk() + + if translation_mode == TranslationModes.PER_PROJECT: + csv_per_project.update_csv_file_on_disk() - new_events += csv_file.new_events - updated_events += csv_file.updated_events + if character_name_csv.is_new_file: + new_timelines += 1 + else: + updated_timelines += 1 - ## ADD CREATION/UPDATE OF CHARACTER NAMES FILE HERE! + new_names += character_name_csv.new_rows + updated_names += character_name_csv.updated_rows - # Silently open the closed timeline. - # May be null, if no timeline was open. - if current_timeline != null: - settings_editor.editors_manager.edit_resource(current_timeline, true, true) + _silently_open_timeline(current_timeline) # Trigger reimport. find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources() - %StatusMessage.text = ("Indexed " + str(new_events) - + " new events ("+ str(updated_events) + " were updated).\n - Added " + str(new_timelines)+ " new CSV files (" - + str(updated_timelines) + " were updated).") + + var status_message := "Events created {new_events} updated {updated_events} + Names created {new_names} updated {updated_names} + CSVs created {new_timelines} updated {updated_timelines}" + + var status_message_args := { + 'new_events': new_events, + 'updated_events': updated_events, + 'new_timelines': new_timelines, + 'updated_timelines': updated_timelines, + 'new_names': new_names, + 'updated_names': updated_names, + } + + %StatusMessage.text = status_message.format(status_message_args) func collect_translations() -> void: @@ -257,61 +330,169 @@ func collect_translations() -> void: func _on_erase_translations_pressed() -> void: - $EraseConfirmationDialog.popup_centered() + %EraseConfirmationDialog.popup_centered() + + +## Deletes the Per-Project CSV file and the character name CSV file. +## Returns `true` on success. +func delete_per_project_csv(translation_folder: String) -> bool: + var per_project_csv := translation_folder.path_join(DEFAULT_TIMELINE_CSV_NAME) + if FileAccess.file_exists(per_project_csv): + if OK == DirAccess.remove_absolute(per_project_csv): + print_rich("[color=green]Deleted Per-Project timeline CSV file: " + per_project_csv + "[/color]") + + # Delete the timeline CSV import file. + DirAccess.remove_absolute(per_project_csv + '.import') + return true + + else: + print_rich("[color=yellow]Failed to delete Per-Project timeline CSV file: " + per_project_csv + "[/color]") + + return false + + +## 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 in DialogicUtil.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 trans_files := Array(ProjectSettings.get_setting('internationalization/locale/translations', [])) - var translation_mode : int = %TransMode.selected + var translation_files := Array(ProjectSettings.get_setting('internationalization/locale/translations', [])) var deleted_csv_files := 0 var deleted_translation_files := 0 + var cleaned_timelines := 0 + var cleaned_characters := 0 + var cleaned_events := 0 - if translation_mode == TranslationModes.PER_PROJECT: - var trans_path :String = ProjectSettings.get_setting('dialogic/translation/translation_folder', 'res://') - DirAccess.remove_absolute(trans_path+'dialogic_translations.csv') - DirAccess.remove_absolute(trans_path+'dialogic_translations.csv.import') - deleted_csv_files += 1 + var current_timeline := _close_active_timeline() - for x_file in DialogicUtil.listdir(trans_path): - if x_file.ends_with('.translation'): - trans_files.erase(trans_path.get_base_dir().path_join(x_file)) - DirAccess.remove_absolute(trans_path.get_base_dir().path_join(x_file)) - deleted_translation_files += 1 + # Delete all Dialogic CSV files and their translation files. + for csv_path in DialogicUtil.list_resources_of_type(".csv"): + var csv_path_parts: PackedStringArray = csv_path.split("/") + var csv_name: String = csv_path_parts[-1].trim_suffix(".csv") - for timeline_path in DialogicUtil.list_resources_of_type('.dtl'): - # in per project mode, remove all translation files/resources next to the timelines - if translation_mode == TranslationModes.PER_TIMELINE: - DirAccess.remove_absolute(timeline_path.trim_suffix('.dtl')+'_translation.csv') - DirAccess.remove_absolute(timeline_path.trim_suffix('.dtl')+'_translation.csv.import') + # 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]") - for x_file in DialogicUtil.listdir(timeline_path.get_base_dir()): + # Clean timelines. + for timeline_path in DialogicUtil.list_resources_of_type(".dtl"): - if x_file.ends_with('.translation'): - trans_files.erase(timeline_path.get_base_dir().path_join(x_file)) - DirAccess.remove_absolute(timeline_path.get_base_dir().path_join(x_file)) - deleted_translation_files += 1 + # Process the timeline. + var timeline: DialogicTimeline = load(timeline_path) + timeline.process() + cleaned_timelines += 1 - # clear the timeline events of their translation_id's - var tml:DialogicTimeline = load(timeline_path) - await tml.process() + # Remove event translation IDs. + for event in timeline.events: - for event in tml.events: - if event._translation_id: + 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 - tml.set_meta("timeline_not_saved", true) - ResourceSaver.save(tml, timeline_path) + timeline.set_meta("timeline_not_saved", true) + ResourceSaver.save(timeline, timeline_path) ProjectSettings.set_setting('dialogic/translation/id_counter', 16) - ProjectSettings.set_setting('internationalization/locale/translations', PackedStringArray(trans_files)) + ProjectSettings.set_setting('internationalization/locale/translations', PackedStringArray(translation_files)) ProjectSettings.save() find_parent('EditorView').plugin_reference.get_editor_interface().get_resource_filesystem().scan_sources() - %StatusMessage.text = ("Erased " +str(deleted_csv_files)+ " CSV files, " - + str(deleted_translation_files) + " translations and all translation ID's.") - _refresh() + var status_message := "Timelines found {cleaned_timelines} + Events cleaned {cleaned_events} + Characters cleaned {cleaned_characters} + + 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, + '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) + + +## 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) diff --git a/addons/dialogic/Editor/Settings/settings_translation.tscn b/addons/dialogic/Editor/Settings/settings_translation.tscn index 677fac832..0da6cbb30 100644 --- a/addons/dialogic/Editor/Settings/settings_translation.tscn +++ b/addons/dialogic/Editor/Settings/settings_translation.tscn @@ -5,7 +5,7 @@ [ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/ComplexPicker.tscn" id="3_dq4j2"] [ext_resource type="PackedScene" uid="uid://7mvxuaulctcq" path="res://addons/dialogic/Editor/Events/Fields/FilePicker.tscn" id="4_kvsma"] -[sub_resource type="Image" id="Image_7t2rc"] +[sub_resource type="Image" id="Image_e2a0f"] data = { "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", @@ -14,8 +14,8 @@ data = { "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_fdokn"] -image = SubResource("Image_7t2rc") +[sub_resource type="ImageTexture" id="ImageTexture_wyypx"] +image = SubResource("Image_e2a0f") [node name="Translations" type="VBoxContainer"] anchors_preset = 15 @@ -71,10 +71,10 @@ text = "Testing locale" [node name="HintTooltip8" parent="HBox/Testing/VBox3" instance=ExtResource("2_k2lou")] layout_mode = 2 -tooltip_text = "Change this locale to test your game in a different language (only in-editor). +tooltip_text = "Change this locale to test your game in a different language (only in-editor). Equivalent of the testing local project setting. " -texture = SubResource("ImageTexture_fdokn") -hint_text = "Change this locale to test your game in a different language (only in-editor). +texture = SubResource("ImageTexture_wyypx") +hint_text = "Change this locale to test your game in a different language (only in-editor). Equivalent of the testing local project setting. " [node name="TestingLocale" parent="HBox/Testing/VBox3" instance=ExtResource("3_dq4j2")] @@ -92,7 +92,7 @@ layout_mode = 2 layout_mode = 2 size_flags_horizontal = 3 -[node name="Title2" type="Label" parent="TranslationSettings/VBoxContainer"] +[node name="SettingsTitle" type="Label" parent="TranslationSettings/VBoxContainer"] layout_mode = 2 theme_type_variation = &"DialogicSettingsSection" text = "Settings" @@ -111,7 +111,7 @@ text = "Default locale" [node name="HintTooltip" parent="TranslationSettings/VBoxContainer/Grid/VBox" instance=ExtResource("2_k2lou")] layout_mode = 2 tooltip_text = "The locale of the language your timelines are written in." -texture = SubResource("ImageTexture_fdokn") +texture = SubResource("ImageTexture_wyypx") hint_text = "The locale of the language your timelines are written in." [node name="OrigLocale" parent="TranslationSettings/VBoxContainer/Grid" instance=ExtResource("3_dq4j2")] @@ -127,11 +127,10 @@ text = "Translation folder" [node name="HintTooltip3" parent="TranslationSettings/VBoxContainer/Grid/TransFile" instance=ExtResource("2_k2lou")] layout_mode = 2 -tooltip_text = "Choose a folder to let Dialogic save CSV files in. - -If the \"Save Location Mode\" is set to \"Inside Translation Folder\", this location will be used." -texture = SubResource("ImageTexture_fdokn") -hint_text = "Choose a folder to let Dialogic save CSV files in. +tooltip_text = "Choose a folder to let Dialogic save CSV files in. +Also used when saving \"Inside Translation Folder\"" +texture = SubResource("ImageTexture_wyypx") +hint_text = "Choose a folder to let Dialogic save CSV files in. Also used when saving \"Inside Translation Folder\"" [node name="TransFolderPicker" parent="TranslationSettings/VBoxContainer/Grid" instance=ExtResource("4_kvsma")] @@ -145,16 +144,23 @@ layout_mode = 2 [node name="Label2" type="Label" parent="TranslationSettings/VBoxContainer/Grid/VBox2"] layout_mode = 2 -text = "File mode" +text = "Timeline mode" [node name="HintTooltip2" parent="TranslationSettings/VBoxContainer/Grid/VBox2" instance=ExtResource("2_k2lou")] layout_mode = 2 -tooltip_text = "Select to create one CSV file with all translatable events from every timeline in the project or one CSV file per timeline." -texture = SubResource("ImageTexture_fdokn") -hint_text = "Decides how many CSV files will be created. +tooltip_text = "Decides how many timeline CSV files will be created. + +\"Per Project\": Uses one CSV file for all timelines (e.g. 30 timelines, 1 CSV) +\"Per Timeline\": Uses one CSV file for each timeline (e.g. 30 timelines, 30 CSVs) + +The \"Per Timeline\" option utilises \"Timeline save location\"." +texture = SubResource("ImageTexture_wyypx") +hint_text = "Decides how many timeline CSV files will be created. + +\"Per Project\": Uses one CSV file for all timelines (e.g. 30 timelines, 1 CSV) +\"Per Timeline\": Uses one CSV file for each timeline (e.g. 30 timelines, 30 CSVs) -\"Per Project\": Uses one CSV file for all timelines. -\"Per Timeline\": Uses one CSV file for each timeline." +The \"Per Timeline\" option utilises \"Timeline save location\"." [node name="TransMode" type="OptionButton" parent="TranslationSettings/VBoxContainer/Grid"] unique_name_in_owner = true @@ -171,24 +177,28 @@ layout_mode = 2 [node name="Label2" type="Label" parent="TranslationSettings/VBoxContainer/Grid/SaveLocation"] layout_mode = 2 -text = "Save location" +text = "Timeline location" [node name="HintTooltip2" parent="TranslationSettings/VBoxContainer/Grid/SaveLocation" instance=ExtResource("2_k2lou")] layout_mode = 2 -tooltip_text = "Select to save CSV files next to their related timelines or inside the defined \"Translation folder\". +tooltip_text = "Decides where to save the generated timeline CSV files. -Note: This button requires the \"Per Timeline\" translation file mode." -texture = SubResource("ImageTexture_fdokn") -hint_text = "Decides where to save the generated CSV files. +- \"Inside Translation Folder\": Uses the \"Translation folder\". +- \"Next To Timeline\": Places them in the timeline's folder. + +This button requires the \"Per Timeline\" timeline mode." +texture = SubResource("ImageTexture_wyypx") +hint_text = "Decides where to save the generated timeline CSV files. - \"Inside Translation Folder\": Uses the \"Translation folder\". - \"Next To Timeline\": Places them in the timeline's folder. -This button requires the \"Per Timeline\" save location." +This button requires the \"Per Timeline\" timeline mode." [node name="SaveLocationMode" type="OptionButton" parent="TranslationSettings/VBoxContainer/Grid"] unique_name_in_owner = true layout_mode = 2 +disabled = true item_count = 2 selected = 0 popup/item_0/text = "Inside Translation Folder" @@ -221,18 +231,20 @@ columns = 2 [node name="UpdateCsvFiles" type="Button" parent="TranslationSettings/VBoxContainer2/Actions"] unique_name_in_owner = true layout_mode = 2 +disabled = true text = "Update CSV files" [node name="HintTooltip5" parent="TranslationSettings/VBoxContainer2/Actions" instance=ExtResource("2_k2lou")] layout_mode = 2 tooltip_text = "This button will scan all timelines and generate or update their CSV files. -Note: This action will be disabled if the \"Translation file mode\" or \"Save location mode\" requires a valid \"Translation folder\" path." -texture = SubResource("ImageTexture_fdokn") +This action will be disabled if the \"Translation folder\" is missing or has an invalid path." +texture = SubResource("ImageTexture_wyypx") hint_text = "This button will scan all timelines and generate or update their CSV files. -This action will be disabled if the \"Translation file mode\" or \"Save location mode\" -requires a valid \"Translation folder\" path." +A Dialogic CSV file will be prefixed with \"dialogic_\". + +This action will be disabled if the \"Translation folder\" is missing or has an invalid path." [node name="CollectTranslations" type="Button" parent="TranslationSettings/VBoxContainer2/Actions"] unique_name_in_owner = true @@ -242,13 +254,11 @@ text = "Collect translations" [node name="HintTooltip6" parent="TranslationSettings/VBoxContainer2/Actions" instance=ExtResource("2_k2lou")] layout_mode = 2 tooltip_text = "Godot imports CSV files as \".translation\" files. -These must be added to \"Project Settings > Localization\" to function. -This button adds new translation files to the settings and removes invalid file paths. +This buttons adds them to \"Project Settings > Localization\". " -texture = SubResource("ImageTexture_fdokn") +texture = SubResource("ImageTexture_wyypx") hint_text = "Godot imports CSV files as \".translation\" files. -These must be added to \"Project Settings > Localization\" to function. -This button adds new translation files to the settings and removes invalid file paths. +This buttons adds them to \"Project Settings -> Localization\". " [node name="AspectRatioContainer2" type="AspectRatioContainer" parent="TranslationSettings/VBoxContainer2/Actions"] @@ -266,18 +276,23 @@ text = "Remove translations" [node name="HintTooltip7" parent="TranslationSettings/VBoxContainer2/Actions" instance=ExtResource("2_k2lou")] layout_mode = 2 -tooltip_text = "Be very careful with this button. It will try to delete any -.csv and .translation files that are related to dialogic. -It will also remove translation ids from all events. " -texture = SubResource("ImageTexture_fdokn") +tooltip_text = "Be very careful with this button! + +It will try to delete any \".csv\" and \".translation\" files that are related to Dialogic. +Removes translation IDs (eg. #id:33) from timelines and characters." +texture = SubResource("ImageTexture_wyypx") hint_text = "Be very careful with this button! -It will try to delete any \".csv\" and \".translation\" files that are related to Dialogic. -Removes translation IDs (eg. #id:33) from timelines." +It will try to delete any \".csv\" and \".translation\" files that are related to Dialogic. +CSV and translation files prefixed with \"dialogic_\" are treated as Dialogic-related. + +Removes translation IDs (eg. #id:33) from timelines and characters." [node name="StatusMessage" type="Label" parent="TranslationSettings/VBoxContainer2"] unique_name_in_owner = true layout_mode = 2 +text = "⛔ Cannot update CSVs files! + Requires valid translation folder to translate character names and the project CSV file." autowrap_mode = 3 [node name="InfoSection" type="VBoxContainer" parent="."] @@ -297,7 +312,7 @@ text = "Setting up translations" layout_mode = 2 size_flags_horizontal = 3 text = "1. Enable translations -2. Set the language you use in your timelines as \"Default timeline\". +2. Set the language you use in your timelines as \"Default timeline\". 3. Select \"File mode\" and \"Save location\". 4. Hit \"Update CSV files\": This creates new CSV files from your timelines. 5. Wait until reimport is finished. Then click \"Collect translations\"." @@ -320,8 +335,8 @@ text = "How to work with translations" layout_mode = 2 size_flags_horizontal = 3 text = "Editing: -You can edit the CSV files in most editors (like Google Sheets or LibreOffice Calc). -Create new languages by adding a new column with a valid locale in the first row. +You can edit the CSV files in most editors (like Google Sheets or LibreOffice Calc). +Create new languages by adding a new column with a valid locale in the first row. Updating: If you made changes to your timelines, hit \"Update CSV files\" again. @@ -334,10 +349,34 @@ While playing, you can use TranslationServer.set_locale(\"your_locale\") " autowrap_mode = 3 +[node name="UpdateConfirmationDialog" type="ConfirmationDialog" parent="."] +unique_name_in_owner = true +title = "Please Decide..." +size = Vector2i(490, 200) +ok_button_text = "Delete old & Generate new" +dialog_text = "You have previously generated CSVs and translation files with different Translation Settings! + +Please consider to delete the old CSVs and then generate new changes." +dialog_autowrap = true + [node name="EraseConfirmationDialog" type="ConfirmationDialog" parent="."] -size = Vector2i(200, 187) -ok_button_text = "Yes" -dialog_text = "This will permanently delete all translation files created by dialogic. Are you sure this is what you want?" +unique_name_in_owner = true +position = Vector2i(0, 36) +size = Vector2i(500, 280) +min_size = Vector2i(300, 70) +ok_button_text = "DELETE ALL" +dialog_text = "You are about to: +- Delete all CSVs prefixed with \"dialogic_\". +- Delete the related CSV import files. +- Delete the related translation files. +- Removes translation IDs from timelines and characters. +- Remove all \"dialogic\" prefixed translations from + \"Project Settings -> Localization\"." dialog_autowrap = true +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="."] +custom_minimum_size = Vector2(0, 31) +layout_mode = 2 + +[connection signal="confirmed" from="UpdateConfirmationDialog" to="." method="_delete_and_update"] [connection signal="confirmed" from="EraseConfirmationDialog" to="." method="erase_translations"] diff --git a/addons/dialogic/Editor/editors_manager.gd b/addons/dialogic/Editor/editors_manager.gd index f16810585..12ac0184a 100644 --- a/addons/dialogic/Editor/editors_manager.gd +++ b/addons/dialogic/Editor/editors_manager.gd @@ -176,7 +176,7 @@ func open_editor(editor:DialogicEditor, save_previous: bool = true, extra_info:V editor_changed.emit(previous_editor, current_editor) -## Rarely used to completely clear a editor. +## Rarely used to completely clear an editor. func clear_editor(editor:DialogicEditor, save:bool = false) -> void: if save: editor._save() diff --git a/addons/dialogic/Modules/Text/subsystem_text.gd b/addons/dialogic/Modules/Text/subsystem_text.gd index 38c950abc..452b6cc7f 100644 --- a/addons/dialogic/Modules/Text/subsystem_text.gd +++ b/addons/dialogic/Modules/Text/subsystem_text.gd @@ -134,11 +134,12 @@ func update_name_label(character:DialogicCharacter) -> void: for name_label in get_tree().get_nodes_in_group('dialogic_name_label'): if character: + var translated_display_name := character.get_display_name_translated() if dialogic.has_subsystem('VAR'): - name_label.text = dialogic.VAR.parse_variables(character.display_name) + name_label.text = dialogic.VAR.parse_variables(translated_display_name) else: - name_label.text = character.display_name + name_label.text = translated_display_name if !'use_character_color' in name_label or name_label.use_character_color: name_label.self_modulate = character.color @@ -363,15 +364,17 @@ func collect_character_names() -> void: return character_colors = {} + for dch_path in DialogicUtil.list_resources_of_type('.dch'): - var dch := (load(dch_path) as DialogicCharacter) + var character := (load(dch_path) as DialogicCharacter) + + if character.display_name: + character_colors[character.display_name] = character.color - if dch.display_name: - character_colors[dch.display_name] = dch.color + for nickname in character.get_nicknames_translated(): - for nickname in dch.nicknames: if nickname.strip_edges(): - character_colors[nickname.strip_edges()] = dch.color + character_colors[nickname.strip_edges()] = character.color color_regex.compile('(?<=\\W|^)(?'+str(character_colors.keys()).trim_prefix('["').trim_suffix('"]').replace('", "', '|')+')(?=\\W|$)') diff --git a/addons/dialogic/Resources/character.gd b/addons/dialogic/Resources/character.gd index 238b81810..012fcbaad 100644 --- a/addons/dialogic/Resources/character.gd +++ b/addons/dialogic/Resources/character.gd @@ -9,7 +9,7 @@ class_name DialogicCharacter @export var color:Color = Color() @export var description:String = "" -@export var scale:float = 1.0 +@export var scale:float = 1.0 @export var offset:Vector2 = Vector2() @export var mirror:bool = false @@ -18,6 +18,14 @@ class_name DialogicCharacter @export var custom_info:Dictionary = {} +## All valid properties that can be accessed by their translation. +enum TranslatedProperties { + NAME, + NICKNAMES, +} + +var _translation_id: String = "" + func __get_property_list() -> Array: return [] @@ -28,6 +36,84 @@ func _to_string() -> String: func _hide_script_from_inspector() -> bool: return true +## This is automatically called, no need to use this. +func add_translation_id() -> String: + _translation_id = DialogicUtil.get_next_translation_id() + return _translation_id + + +func remove_translation_id() -> void: + _translation_id = "" + +## Checks [param property] and matches it to a translation key. +## +## Undefined behaviour if an invalid integer is passed. +func get_property_translation_key(property: TranslatedProperties) -> String: + var property_key := "" + + match property: + TranslatedProperties.NAME: + property_key = "name" + TranslatedProperties.NICKNAMES: + property_key = "nicknames" + + return "Character".path_join(_translation_id).path_join(property_key) + + +## Accesses the original text of the character. +## +## Undefined behaviour if an invalid integer is passed. +func _get_property_original_text(property: TranslatedProperties) -> String: + match property: + TranslatedProperties.NAME: + return display_name + TranslatedProperties.NICKNAMES: + return ", ".join(nicknames) + + return "" + + +## Access a property of the character and if conditions are met, attempts to +## translate the property. +## +## The translation feature must be enabled in the project settings. +## The translation ID must be set. +## Otherwise, returns the text property as is. +## +## Undefined behaviour if an invalid integer is passed. +func _get_property_translated(property: TranslatedProperties) -> String: + var try_translation: bool = (_translation_id != null + and not _translation_id.is_empty() + and ProjectSettings.get_setting('dialogic/translation/enabled', false) + ) + + if try_translation: + var translation_key := get_property_translation_key(property) + var translated_property := tr(translation_key) + + # If no translation is found, tr() returns the ID. + # However, we want to fallback to the original text. + if translated_property == translation_key: + return _get_property_original_text(property) + + return translated_property + + else: + return _get_property_original_text(property) + + +## Translates the nicknames of the characters and then returns them as an array +## of strings. +func get_nicknames_translated() -> Array: + var translated_nicknames := _get_property_translated(TranslatedProperties.NICKNAMES) + return (translated_nicknames.split(", ") as Array) + + +## Translates and returns the display name of the character. +func get_display_name_translated() -> String: + return _get_property_translated(TranslatedProperties.NAME) + + ## Returns the name of the file (without the extension). func get_character_name() -> String: if !resource_path.is_empty(): diff --git a/addons/dialogic/Resources/event.gd b/addons/dialogic/Resources/event.gd index 9945b1abd..9ccb3393c 100644 --- a/addons/dialogic/Resources/event.gd +++ b/addons/dialogic/Resources/event.gd @@ -195,7 +195,7 @@ func get_property_translated(property_name:String) -> String: if !_translation_id.is_empty() and ProjectSettings.get_setting('dialogic/translation/enabled', false): var translation = tr(get_property_translation_key(property_name)) # if no translation is found tr() returns the id, but we want to fallback to the original - return translation if translation != _translation_id else _get_property_original_translation(property_name) + return translation if translation != get_property_translation_key(property_name) else _get_property_original_translation(property_name) else: return _get_property_original_translation(property_name)