From b720707c279a25260f2565e4894b4ea642845953 Mon Sep 17 00:00:00 2001 From: jrb0001 Date: Fri, 2 Aug 2024 18:22:01 +0200 Subject: [PATCH] Implement gettext translation file generation --- .../Editor/Settings/TranslationFiles/csv.gd | 124 ++++++ .../Settings/TranslationFiles/gettext.gd | 148 +++++++ .../Settings/TranslationFiles/shared.gd | 150 ++++++++ addons/dialogic/Editor/Settings/csv_file.gd | 356 ----------------- .../Editor/Settings/settings_translation.gd | 360 +++++++++--------- .../Editor/Settings/settings_translation.tscn | 145 ++++--- .../Modules/Glossary/glossary_resource.gd | 19 +- .../Modules/Glossary/subsystem_glossary.gd | 20 +- addons/dialogic/Resources/event.gd | 4 + addons/dialogic/Resources/timeline.gd | 2 + 10 files changed, 714 insertions(+), 614 deletions(-) create mode 100644 addons/dialogic/Editor/Settings/TranslationFiles/csv.gd create mode 100644 addons/dialogic/Editor/Settings/TranslationFiles/gettext.gd create mode 100644 addons/dialogic/Editor/Settings/TranslationFiles/shared.gd delete mode 100644 addons/dialogic/Editor/Settings/csv_file.gd diff --git a/addons/dialogic/Editor/Settings/TranslationFiles/csv.gd b/addons/dialogic/Editor/Settings/TranslationFiles/csv.gd new file mode 100644 index 000000000..1fb466d9f --- /dev/null +++ b/addons/dialogic/Editor/Settings/TranslationFiles/csv.gd @@ -0,0 +1,124 @@ +class_name DialogicTranslationCsvFile +extends DialogicTranslationFile +## Generates translation files in CSV format. + +var lines: Array[PackedStringArray] = [] +## Dictionary of lines from the original file. +## Key: String, Value: PackedStringArray +var old_lines: Dictionary = {} + +## The amount of columns the CSV file has after loading it. +## Used to add trailing commas to new lines. +var column_count := 0 + +## The underlying file used to read and write the CSV file. +var file: FileAccess + +## Whether this CSV handler should add newlines as a separator between sections. +## A section may be a new character, new timeline, or new glossary item inside +## a per-project file. +var add_separator: bool = false + + +## 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. +## The [param separator_enabled] enables adding newlines as a separator to +## per-project files. This is useful for readability. +func _init(file_path: String, original_locale: String, separator_enabled: bool) -> void: + super._init(file_path, original_locale) + + add_separator = separator_enabled + + # 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 is_new_file: + # The "keys" and original locale are the only columns in a new file. + # For example: "keys, en" + column_count = 2 + return + + 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] + + old_lines[locale_key] = locale_csv_row + + _read_file_into_lines() + + +## Private function to read the CSV file into the [member lines] array. +## Cannot be called on a new file. +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 + + +func _append(key: String, value: String, _path: String, _line_number: int = -1) -> void: + var array_line := PackedStringArray([key, value]) + lines.append(array_line) + + +## Appends an empty line to the [member lines] array. +func _append_separator() -> void: + if add_separator: + var empty_line := PackedStringArray(["", ""]) + lines.append(empty_line) + + +## Clears the CSV file on disk and writes the current [member lines] array to it. +## Uses the [member old_lines] dictionary to update existing translations. +## If a translation row misses a column, a trailing comma will be added to +## conform to the CSV file format. +## +## If the locale CSV line was collected only, a new file won't be created and +## already existing translations won't be updated. +func update_file_on_disk() -> void: + # None or locale row only. + if lines.size() < 2: + print_rich("[color=yellow]No lines for the CSV file, skipping: " + used_file_path) + + return + + # Clear the current CSV file. + file = FileAccess.open(used_file_path, FileAccess.WRITE) + + 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) + + 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("") + + 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 + + # 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_rows += 1 + + file.close() diff --git a/addons/dialogic/Editor/Settings/TranslationFiles/gettext.gd b/addons/dialogic/Editor/Settings/TranslationFiles/gettext.gd new file mode 100644 index 000000000..137d2876b --- /dev/null +++ b/addons/dialogic/Editor/Settings/TranslationFiles/gettext.gd @@ -0,0 +1,148 @@ +class_name DialogicTranslationGettextFile +extends DialogicTranslationFile +## Generates translation files in gettext format. + +var translations: Array[PotEntry] = [] + +## Configured original locale. +var original_locale: String + +## Locations of the source files included in this translation. +var locations: Array[String] = [] + + +## There is no need to load the old file(s) here, because every locale has its own file +## and this class doens't touch them. +func _init(file_path: String, original_locale: String) -> void: + super._init(file_path, original_locale) + self.original_locale = original_locale + + +func _append(key: String, value: String, path: String, line_number: int = -1) -> void: + var entry = PotEntry.new() + entry.key = key + entry.translation = value + entry.locations.append(PotReference.new(path, line_number)) + translations.append(entry) + + +## gettext doesn't support separators so this is a no-op. +func _append_separator() -> void: + pass + + +## Overwrites the .pot file and the .po file of the original locale with the current [member translations] array. +func update_file_on_disk() -> void: + # Overwrite the POT file. + var file = FileAccess.open(used_file_path, FileAccess.WRITE) + _write_header(file) + for entry in translations: + _write_entry(file, entry, "") + file.close() + + # Overwrite the original_locale PO file. + file = FileAccess.open(used_file_path.trim_suffix(".pot") + "." + original_locale + ".po", FileAccess.WRITE) + _write_header(file, original_locale) + for entry in translations: + _write_entry(file, entry) + file.close() + + +# This is based on POTGenerator::_write_to_pot() which unfortunately isn't exposed to gdscript. +func _write_header(file: FileAccess, locale: String = "") -> void: + var project_name = ProjectSettings.get("application/config/name"); + var language_header = locale if !locale.is_empty() else "LANGUAGE" + file.store_line("# " + language_header + " translation for " + project_name + " for the following files:") + + locations.sort() + for location in locations: + file.store_line("# " + location) + + file.store_line("") + file.store_line("#, fuzzy"); + file.store_line("msgid \"\"") + file.store_line("msgstr \"\"") + file.store_line("\"Project-Id-Version: " + project_name + "\\n\"") + if !locale.is_empty(): + file.store_line("\"Language: " + locale + "\\n\"") + file.store_line("\"MIME-Version: 1.0\\n\"") + file.store_line("\"Content-Type: text/plain; charset=UTF-8\\n\"") + file.store_line("\"Content-Transfer-Encoding: 8-bit\\n\"") + + +func _write_entry(file: FileAccess, entry: PotEntry, value: String = entry.translation) -> void: + file.store_line("") + + entry.locations.sort_custom(func (a: String, b: String): return b > a) + for location in entry.locations: + file.store_line("#: " + location.as_str()) + + _write_line(file, "msgid", entry.key) + _write_line(file, "msgstr", value) + + +# This is based on POTGenerator::_write_msgid() which unfortunately isn't exposed to gdscript. +func _write_line(file: FileAccess, type: String, value: String) -> void: + file.store_string(type + " ") + if value.is_empty(): + file.store_line("\"\"") + return + + var lines = value.split("\n") + var last_line = lines[lines.size() - 1] + var pot_line_count = lines.size() + if last_line.is_empty(): + pot_line_count -= 1 + + if pot_line_count > 1: + file.store_line("\"\"") + + for i in range(0, lines.size() - 1): + file.store_line("\"" + (lines[i] + "\n").json_escape() + "\"") + + if !last_line.is_empty(): + file.store_line("\"" + last_line.json_escape() + "\"") + + +func collect_lines_from_character(character: DialogicCharacter) -> void: + super.collect_lines_from_character(character) + locations.append(character.resource_path) + + +func collect_lines_from_glossary(glossary: DialogicGlossary) -> void: + super.collect_lines_from_glossary(glossary) + locations.append(glossary.resource_path) + + +func collect_lines_from_timeline(timeline: DialogicTimeline) -> void: + super.collect_lines_from_timeline(timeline) + locations.append(timeline.resource_path) + + +class PotReference: + var path: String + var line_number: int + + + func _init(path: String, line_number: int) -> void: + self.path = path + self.line_number = line_number + + + func as_str() -> String: + var str = "" + if path.contains(" "): + str += "\u2068" + path.trim_prefix("res://").replace("\n", "\\n") + "\u2069" + else: + str += path.trim_prefix("res://").replace("\n", "\\n") + + if line_number >= 0: + str += ":" + str(line_number) + + return str + + +class PotEntry: + var key: String + var translation: String + var locations: Array[PotReference] = [] diff --git a/addons/dialogic/Editor/Settings/TranslationFiles/shared.gd b/addons/dialogic/Editor/Settings/TranslationFiles/shared.gd new file mode 100644 index 000000000..5767c55a6 --- /dev/null +++ b/addons/dialogic/Editor/Settings/TranslationFiles/shared.gd @@ -0,0 +1,150 @@ +class_name DialogicTranslationFile +extends RefCounted +## Generates translation files for [class DialogicTimeline], [class DialogicGlossary] and [class DialogicCharacter]. + +## Whether this file was able to be loaded a defined +## file path. +var is_new_file: bool = false + +## File path used to load the file. +var used_file_path: String + +## The amount of events that were updated in the file. +var updated_rows: int = 0 + +## The amount of events that were added to the file. +var new_rows: int = 0 + + +func _init(file_path: String, original_locale: String) -> void: + used_file_path = file_path + is_new_file = not FileAccess.file_exists(file_path) + + +## Append a new entry to the available translations. +func _append(_key: String, _value: String, _path: String, _line_number: int = -1) -> void: + pass + + +## Appends a separator if supported by the format and enabled by the user. +func _append_separator() -> void: + pass + + +## Clears the file on disk and writes the current translations to it. +func update_file_on_disk() -> void: + pass + + +## Collects names from the given [param character] and adds them. +func collect_lines_from_character(character: DialogicCharacter) -> void: + # 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 + _append(display_name_key, line_value, character.resource_path) + + var nicknames: Array = character.nicknames + + if not nicknames.is_empty(): + var nick_name_property := DialogicCharacter.TranslatedProperties.NICKNAMES + var nickname_string: String = ",".join(nicknames) + var nickname_name_line_key: String = character.get_property_translation_key(nick_name_property) + _append(nickname_name_line_key, nickname_string, character.resource_path) + + # New character item, if needed, add a separator. + _append_separator() + + +## Returns whether [param value_b] is greater than [param value_a]. +## +## This method helps to sort glossary entry properties by their importance +## matching the order in the editor. +## +## TODO: Allow Dialogic users to define their own order. +func _sort_glossary_entry_property_keys(property_key_a: String, property_key_b: String) -> bool: + const GLOSSARY_LINE_ORDER := { + DialogicGlossary.NAME_PROPERTY: 0, + DialogicGlossary.ALTERNATIVE_PROPERTY: 1, + DialogicGlossary.TEXT_PROPERTY: 2, + DialogicGlossary.EXTRA_PROPERTY: 3, + } + const UNKNOWN_PROPERTY_ORDER := 100 + + var value_a: int = GLOSSARY_LINE_ORDER.get(property_key_a, UNKNOWN_PROPERTY_ORDER) + var value_b: int = GLOSSARY_LINE_ORDER.get(property_key_b, UNKNOWN_PROPERTY_ORDER) + + return value_a < value_b + + +## Collects properties from glossary entries from the given [param glossary] and +## adds them. +func collect_lines_from_glossary(glossary: DialogicGlossary) -> void: + + for glossary_value: Variant in glossary.entries.values(): + + if glossary_value is String: + continue + + var glossary_entry: Dictionary = glossary_value + var glossary_entry_name: String = glossary_entry[DialogicGlossary.NAME_PROPERTY] + + var _glossary_translation_id := glossary.get_set_glossary_translation_id() + var entry_translation_id := glossary.get_set_glossary_entry_translation_id(glossary_entry_name) + + var entry_property_keys := glossary_entry.keys().duplicate() + entry_property_keys.sort_custom(_sort_glossary_entry_property_keys) + + var entry_name_property: String = glossary_entry[DialogicGlossary.NAME_PROPERTY] + + for entry_key: String in entry_property_keys: + # Ignore private keys. + if entry_key.begins_with(DialogicGlossary.PRIVATE_PROPERTY_PREFIX): + continue + + var item_value: Variant = glossary_entry[entry_key] + var item_value_str := "" + + if item_value is Array: + var item_array := item_value as Array + # We use a space after the comma to make it easier to read. + item_value_str = ", ".join(item_array) + + elif not item_value is String or item_value.is_empty(): + continue + + else: + item_value_str = item_value + + var glossary_key := glossary._get_glossary_translation_key(entry_translation_id, entry_key) + + if (entry_key == DialogicGlossary.NAME_PROPERTY + or entry_key == DialogicGlossary.ALTERNATIVE_PROPERTY): + glossary.entries[glossary_key] = entry_name_property + + _append(glossary_key, item_value_str, glossary.resource_path, -1) + + # New glossary item, if needed, add a separator. + _append_separator() + + +## Collects translatable events from the given [param timeline] and adds +## them. +func collect_lines_from_timeline(timeline: DialogicTimeline) -> void: + for event: DialogicEvent in timeline.events: + + if event.can_be_translated(): + + if event._translation_id.is_empty(): + event.add_translation_id() + event.update_text_version() + + var properties: Array = event._get_translatable_properties() + + for property: String in properties: + var line_key: String = event.get_property_translation_key(property) + var line_value: String = event._get_property_original_translation(property) + _append(line_key, line_value, event.source_path, event.source_line_number) + + # End of timeline, if needed, add a separator. + _append_separator() diff --git a/addons/dialogic/Editor/Settings/csv_file.gd b/addons/dialogic/Editor/Settings/csv_file.gd deleted file mode 100644 index 3f8fb3a98..000000000 --- a/addons/dialogic/Editor/Settings/csv_file.gd +++ /dev/null @@ -1,356 +0,0 @@ -class_name DialogicCsvFile -extends RefCounted -## Handles translation of a [class DialogicTimeline] to a CSV file. - -var lines: Array[PackedStringArray] = [] -## Dictionary of lines from the original file. -## Key: String, Value: PackedStringArray -var old_lines: Dictionary = {} - -## The amount of columns the CSV file has after loading it. -## Used to add trailing commas to new lines. -var column_count := 0 - -## Whether this CSV file was able to be loaded a defined -## file path. -var is_new_file: bool = false - -## The underlying file used to read and write the CSV file. -var file: FileAccess - -## File path used to load the CSV file. -var used_file_path: String - -## The amount of events that were updated in the CSV file. -var updated_rows: int = 0 - -## The amount of events that were added to the CSV file. -var new_rows: int = 0 - -## Whether this CSV handler should add newlines as a separator between sections. -## A section may be a new character, new timeline, or new glossary item inside -## a per-project file. -var add_separator: bool = false - -enum PropertyType { - String = 0, - Array = 1, - Other = 2, -} - -## The translation property used for the glossary item translation. -const TRANSLATION_ID := DialogicGlossary.TRANSLATION_PROPERTY - -## 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. -## The [param separator_enabled] enables adding newlines as a separator to -## per-project files. This is useful for readability. -func _init(file_path: String, original_locale: String, separator_enabled: bool) -> void: - used_file_path = file_path - add_separator = separator_enabled - - # 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 ResourceLoader.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 - return - - 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] - - old_lines[locale_key] = locale_csv_row - - _read_file_into_lines() - - -## Private function to read the CSV file into the [member lines] array. -## Cannot be called on a new file. -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 - - -## 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: DialogicCharacter in characters.values(): - # 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 nicknames: Array = character.nicknames - - if not nicknames.is_empty(): - var nick_name_property := DialogicCharacter.TranslatedProperties.NICKNAMES - var nickname_string: String = ",".join(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) - - # New character item, if needed, add a separator. - if add_separator: - _append_empty() - - -## Appends an empty line to the [member lines] array. -func _append_empty() -> void: - var empty_line := PackedStringArray(["", ""]) - lines.append(empty_line) - - -## Returns the property type for the given [param key]. -func _get_key_type(key: String) -> PropertyType: - if key.ends_with(DialogicGlossary.NAME_PROPERTY): - return PropertyType.String - - if key.ends_with(DialogicGlossary.ALTERNATIVE_PROPERTY): - return PropertyType.Array - - return PropertyType.Other - - -func _process_line_into_array(csv_values: PackedStringArray, property_type: PropertyType) -> Array[String]: - const KEY_VALUE_INDEX := 0 - var values_as_array: Array[String] = [] - - for i in csv_values.size(): - - if i == KEY_VALUE_INDEX: - continue - - var csv_value := csv_values[i] - - if csv_value.is_empty(): - continue - - match property_type: - PropertyType.String: - values_as_array = [csv_value] - - PropertyType.Array: - var split_values := csv_value.split(",") - - for value in split_values: - values_as_array.append(value) - - return values_as_array - - -func _add_keys_to_glossary(glossary: DialogicGlossary, names: Array) -> void: - var glossary_prefix_key := glossary._get_glossary_translation_id_prefix() - var glossary_translation_id_prefix := _get_glossary_translation_key_prefix(glossary) - - for glossary_line: PackedStringArray in names: - - if glossary_line.is_empty(): - continue - - var csv_key := glossary_line[0] - - # CSV line separators will be empty. - if not csv_key.begins_with(glossary_prefix_key): - continue - - var value_type := _get_key_type(csv_key) - - # String and Array are the only valid types. - if (value_type == PropertyType.Other - or not csv_key.begins_with(glossary_translation_id_prefix)): - continue - - var new_line_to_add := _process_line_into_array(glossary_line, value_type) - - for name_to_add: String in new_line_to_add: - glossary._translation_keys[name_to_add.strip_edges()] = csv_key - - - -## Reads all [member lines] and adds them to the given [param glossary]'s -## internal collection of words-to-translation-key mappings. -## -## Populate the CSV's lines with the method [method collect_lines_from_glossary] -## before. -func add_translation_keys_to_glossary(glossary: DialogicGlossary) -> void: - glossary._translation_keys.clear() - _add_keys_to_glossary(glossary, lines) - _add_keys_to_glossary(glossary, old_lines.values()) - - -## Returns the translation key prefix for the given [param glossary_translation_id]. -## The resulting format will look like this: Glossary/a2/ -## You can use this to find entries in [member lines] that to a glossary. -func _get_glossary_translation_key_prefix(glossary: DialogicGlossary) -> String: - return ( - DialogicGlossary.RESOURCE_NAME - .path_join(glossary._translation_id) - ) - - -## Returns whether [param value_b] is greater than [param value_a]. -## -## This method helps to sort glossary entry properties by their importance -## matching the order in the editor. -## -## TODO: Allow Dialogic users to define their own order. -func _sort_glossary_entry_property_keys(property_key_a: String, property_key_b: String) -> bool: - const GLOSSARY_CSV_LINE_ORDER := { - DialogicGlossary.NAME_PROPERTY: 0, - DialogicGlossary.ALTERNATIVE_PROPERTY: 1, - DialogicGlossary.TEXT_PROPERTY: 2, - DialogicGlossary.EXTRA_PROPERTY: 3, - } - const UNKNOWN_PROPERTY_ORDER := 100 - - var value_a: int = GLOSSARY_CSV_LINE_ORDER.get(property_key_a, UNKNOWN_PROPERTY_ORDER) - var value_b: int = GLOSSARY_CSV_LINE_ORDER.get(property_key_b, UNKNOWN_PROPERTY_ORDER) - - return value_a < value_b - - -## Collects properties from glossary entries from the given [param glossary] and -## adds them to the [member lines]. -func collect_lines_from_glossary(glossary: DialogicGlossary) -> void: - - for glossary_value: Variant in glossary.entries.values(): - - if glossary_value is String: - continue - - var glossary_entry: Dictionary = glossary_value - var glossary_entry_name: String = glossary_entry[DialogicGlossary.NAME_PROPERTY] - - var _glossary_translation_id := glossary.get_set_glossary_translation_id() - var entry_translation_id := glossary.get_set_glossary_entry_translation_id(glossary_entry_name) - - var entry_property_keys := glossary_entry.keys().duplicate() - entry_property_keys.sort_custom(_sort_glossary_entry_property_keys) - - var entry_name_property: String = glossary_entry[DialogicGlossary.NAME_PROPERTY] - - for entry_key: String in entry_property_keys: - # Ignore private keys. - if entry_key.begins_with(DialogicGlossary.PRIVATE_PROPERTY_PREFIX): - continue - - var item_value: Variant = glossary_entry[entry_key] - var item_value_str := "" - - if item_value is Array: - var item_array := item_value as Array - # We use a space after the comma to make it easier to read. - item_value_str = " ,".join(item_array) - - elif not item_value is String or item_value.is_empty(): - continue - - else: - item_value_str = item_value - - var glossary_csv_key := glossary._get_glossary_translation_key(entry_translation_id, entry_key) - - if (entry_key == DialogicGlossary.NAME_PROPERTY - or entry_key == DialogicGlossary.ALTERNATIVE_PROPERTY): - glossary.entries[glossary_csv_key] = entry_name_property - - var glossary_line := PackedStringArray([glossary_csv_key, item_value_str]) - - lines.append(glossary_line) - - # New glossary item, if needed, add a separator. - if add_separator: - _append_empty() - - - -## Collects translatable events from the given [param timeline] and adds -## them to the [member lines]. -func collect_lines_from_timeline(timeline: DialogicTimeline) -> void: - for event: DialogicEvent in timeline.events: - - if event.can_be_translated(): - - if event._translation_id.is_empty(): - event.add_translation_id() - event.update_text_version() - - var properties: Array = event._get_translatable_properties() - - for property: String 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) - - # End of timeline, if needed, add a separator. - if add_separator: - _append_empty() - - -## Clears the CSV file on disk and writes the current [member lines] array to it. -## Uses the [member old_lines] dictionary to update existing translations. -## If a translation row misses a column, a trailing comma will be added to -## conform to the CSV file format. -## -## If the locale CSV line was collected only, a new file won't be created and -## already existing translations won't be updated. -func update_csv_file_on_disk() -> void: - # None or locale row only. - if lines.size() < 2: - print_rich("[color=yellow]No lines for the CSV file, skipping: " + used_file_path) - - return - - # Clear the current CSV file. - file = FileAccess.open(used_file_path, FileAccess.WRITE) - - 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) - - 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("") - - 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 - - # 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_rows += 1 - - file.close() diff --git a/addons/dialogic/Editor/Settings/settings_translation.gd b/addons/dialogic/Editor/Settings/settings_translation.gd index fe0714edd..fda8d2439 100644 --- a/addons/dialogic/Editor/Settings/settings_translation.gd +++ b/addons/dialogic/Editor/Settings/settings_translation.gd @@ -1,29 +1,31 @@ @tool extends DialogicSettingsPage -## Settings tab that allows enabeling and updating translation csv-files. +## Settings tab that allows enabeling and updating translation files. enum TranslationModes {PER_PROJECT, PER_TIMELINE, NONE} enum SaveLocationModes {INSIDE_TRANSLATION_FOLDER, NEXT_TO_TIMELINE, NONE} +enum FileFormat {CSV, GETTEXT} 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. +## The default filename without extension that contains the translations for +## character properties. +const DEFAULT_CHARACTER_FILE_NAME := "dialogic_character_translations" +## The default filename without extension 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_TIMELINE_TRANSLATION_FILE_NAME := "dialogic_timeline_translations" -const DEFAULT_GLOSSARY_CSV_NAME := "dialogic_glossary_translations.csv" +const DEFAULT_GLOSSARY_TRANSLATION_FILE_NAME := "dialogic_glossary_translations" 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. +## Unique locales that will be set after updating the files. var _unique_locales := [] func _get_icon() -> Texture2D: @@ -47,9 +49,10 @@ func _ready() -> void: %SaveLocationMode.item_selected.connect(store_changes) %TransMode.item_selected.connect(store_changes) + %FileFormat.item_selected.connect(store_changes) - %UpdateCsvFiles.pressed.connect(_on_update_translations_pressed) - %UpdateCsvFiles.icon = get_theme_icon("Add", "EditorIcons") + %UpdateTranslationFiles.pressed.connect(_on_update_translations_pressed) + %UpdateTranslationFiles.icon = get_theme_icon("Add", "EditorIcons") %CollectTranslations.pressed.connect(collect_translations) %CollectTranslations.icon = get_theme_icon("File", "EditorIcons") @@ -57,7 +60,7 @@ func _ready() -> void: %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.add_button("Keep old & Generate new", false, "keep_old_add_new") %UpdateConfirmationDialog.custom_action.connect(_on_custom_action) @@ -65,8 +68,9 @@ func _ready() -> void: func _on_custom_action(action: String) -> void: - if action == "generate_new": - update_csv_files() + if action == "keep_old_add_new": + update_translation_files() + %UpdateConfirmationDialog.hide() func _refresh() -> void: @@ -76,6 +80,7 @@ func _refresh() -> void: %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)) + %FileFormat.select(ProjectSettings.get_setting('dialogic/translation/file_format', FileFormat.CSV)) %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) @@ -95,6 +100,7 @@ func store_changes(_fake_arg: Variant = null, _fake_arg2: Variant = null) -> voi %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/file_format', %FileFormat.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) @@ -103,7 +109,7 @@ func store_changes(_fake_arg: Variant = null, _fake_arg2: Variant = null) -> voi ## Checks whether the translation folder path is required. -## If it is, disables the "Update CSV files" button and shows a warning. +## If it is, disables the "Update translation 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". @@ -120,7 +126,7 @@ func _verify_translation_file() -> void: var valid_translation_folder := (!translation_folder.is_empty() and DirAccess.dir_exists_absolute(translation_folder)) - %UpdateCsvFiles.disabled = not valid_translation_folder + %UpdateTranslationFiles.disabled = not valid_translation_folder var status_message := "" @@ -128,7 +134,7 @@ func _verify_translation_file() -> void: status_message += "⛔ Requires valid translation folder to translate character names" if file_mode == TranslationModes.PER_PROJECT: - status_message += " and the project CSV file." + status_message += " and the translation file." else: status_message += "." @@ -157,16 +163,19 @@ func get_locales(_filter: String) -> Dictionary: func _on_update_translations_pressed() -> void: var save_mode: SaveLocationModes = %SaveLocationMode.selected var file_mode: TranslationModes = %TransMode.selected + var file_format: FileFormat = %FileFormat.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_file_format: FileFormat = ProjectSettings.get_setting('dialogic/translation/intern/file_format', file_format) 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_file_format == file_format and old_translation_folder == translation_folder): - update_csv_files() + update_translation_files() return %UpdateConfirmationDialog.popup_centered() @@ -175,74 +184,71 @@ func _on_update_translations_pressed() -> void: ## Used by the dialog to inform that the settings were changed. func _delete_and_update() -> void: erase_translations() - update_csv_files() + update_translation_files() -## Creates or updates the glossary CSV files. +## Creates or updates the glossary translation files. func _handle_glossary_translation( - csv_data: CsvUpdateData, + translation_data: TranslationUpdateData, save_location_mode: SaveLocationModes, translation_mode: TranslationModes, translation_folder_path: String, orig_locale: String) -> void: - var glossary_csv: DialogicCsvFile = null + var translation_file: DialogicTranslationFile = 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 := "" + if translation_file == null: + var file_name := "" - # Get glossary CSV file name. + # Get glossary translation file name. match translation_mode: TranslationModes.PER_PROJECT: - csv_name = DEFAULT_GLOSSARY_CSV_NAME + file_name = DEFAULT_GLOSSARY_TRANSLATION_FILE_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' + file_name = "dialogic_" + path_parts[-1] + '_translation' - var glossary_csv_path := "" - # Get glossary CSV file path. + var translation_file_path := "" + # Get glossary translation file path. match save_location_mode: SaveLocationModes.INSIDE_TRANSLATION_FOLDER: - glossary_csv_path = translation_folder_path.path_join(csv_name) + translation_file_path = translation_folder_path.path_join(file_name) SaveLocationModes.NEXT_TO_TIMELINE: - glossary_csv_path = glossary_path.get_base_dir().path_join(csv_name) + translation_file_path = glossary_path.get_base_dir().path_join(file_name) - # Create or update glossary CSV file. - glossary_csv = DialogicCsvFile.new(glossary_csv_path, orig_locale, add_separator_lines) + # Create or update glossary translation file. + translation_file = _open_translation_file(translation_file_path, orig_locale) - if (glossary_csv.is_new_file): - csv_data.new_glossaries += 1 + if (translation_file.is_new_file): + translation_data.new_glossaries += 1 else: - csv_data.updated_glossaries += 1 + translation_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) + translation_file.collect_lines_from_glossary(glossary) ResourceSaver.save(glossary) - #If per-file mode is used, save this csv and begin a new one + #If per-file mode is used, save this file and begin a new one. if translation_mode == TranslationModes.PER_TIMELINE: - glossary_csv.update_csv_file_on_disk() - glossary_csv = null + translation_file.update_file_on_disk() + translation_file = 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 + if translation_file != null: + translation_file.update_file_on_disk() + translation_file = null -## Keeps information about the amount of new and updated CSV rows and what -## resources were populated with translation IDs. +## Keeps information about the amount of new and updated translation entries +## and what resources were populated with translation IDs. ## The final data can be used to display a status message. -class CsvUpdateData: +class TranslationUpdateData: var new_events := 0 var updated_events := 0 @@ -259,43 +265,44 @@ class CsvUpdateData: var updated_glossary_entries := 0 -func update_csv_files() -> void: +func update_translation_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 file_format: FileFormat = ProjectSettings.get_setting('dialogic/translation/file_format', FileFormat.CSV) 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() + var translation_data := TranslationUpdateData.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/file_format', file_format) 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) + var file_per_project: DialogicTranslationFile = null + var per_project_file_path := translation_folder_path.path_join(DEFAULT_TIMELINE_TRANSLATION_FILE_NAME) if translation_mode == TranslationModes.PER_PROJECT: - csv_per_project = DialogicCsvFile.new(per_project_csv_path, orig_locale, add_separator_lines) + file_per_project = _open_translation_file(per_project_file_path, orig_locale) - if (csv_per_project.is_new_file): - csv_data.new_timelines += 1 + if (file_per_project.is_new_file): + translation_data.new_timelines += 1 else: - csv_data.updated_timelines += 1 + translation_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. + # Create or update translation files. + # Transform the timeline into translatable lines and collect into the translation file. for timeline_path: String in DialogicResourceUtil.list_resources_of_type('.dtl'): - var csv_file: DialogicCsvFile = csv_per_project + var translation_file: DialogicTranslationFile = file_per_project - # Swap the CSV file to the Per Timeline one. + # Swap the translation 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("/") @@ -307,9 +314,9 @@ func update_csv_files() -> void: 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 + per_timeline_path += '_translation' + translation_file = _open_translation_file(per_timeline_path, orig_locale) + translation_data.new_timelines += 1 # Load and process timeline, turn events into resources. var timeline: DialogicTimeline = load(timeline_path) @@ -320,21 +327,21 @@ func update_csv_files() -> void: timeline.process() - # Collect timeline into CSV. - csv_file.collect_lines_from_timeline(timeline) + # Collect timeline into translation file. + translation_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() + translation_file.update_file_on_disk() - csv_data.new_events += csv_file.new_rows - csv_data.updated_events += csv_file.updated_rows + translation_data.new_events += translation_file.new_rows + translation_data.updated_events += translation_file.updated_rows _handle_glossary_translation( - csv_data, + translation_data, save_location_mode, translation_mode, translation_folder_path, @@ -342,14 +349,13 @@ func update_csv_files() -> void: ) _handle_character_names( - csv_data, + translation_data, orig_locale, - translation_folder_path, - add_separator_lines + translation_folder_path ) if translation_mode == TranslationModes.PER_PROJECT: - csv_per_project.update_csv_file_on_disk() + file_per_project.update_file_on_disk() _silently_open_timeline(current_timeline) @@ -358,96 +364,72 @@ func update_csv_files() -> void: var status_message := "Events created {new_events} found {updated_events} Names created {new_names} found {updated_names} - CSVs created {new_timelines} found {updated_timelines} + Files 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, + 'new_events': translation_data.new_events, + 'updated_events': translation_data.updated_events, + 'new_timelines': translation_data.new_timelines, + 'updated_timelines': translation_data.updated_timelines, + 'new_glossaries': translation_data.new_glossaries, + 'updated_glossaries': translation_data.updated_glossaries, + 'new_names': translation_data.new_names, + 'updated_names': translation_data.updated_names, + 'new_glossary_entries': translation_data.new_glossary_entries, + 'updated_glossary_entries': translation_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 +## Iterates over all character resource files and creates or updates translation files ## that contain the translations for character properties. ## This will save each character resource file to disk. func _handle_character_names( - csv_data: CsvUpdateData, + translation_data: TranslationUpdateData, 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 := {} + translation_folder_path: String) -> void: + var names_translation_path := translation_folder_path.path_join(DEFAULT_CHARACTER_FILE_NAME) + var character_name_file: DialogicTranslationFile = _open_translation_file(names_translation_path, original_locale) 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 + translation_data.new_names += 1 else: - csv_data.updated_names += 1 - - var translation_id := character.get_set_translation_id() - all_characters[translation_id] = character + translation_data.updated_names += 1 ResourceSaver.save(character) + character_name_file.collect_lines_from_character(character) - character_name_csv.collect_lines_from_characters(all_characters) - character_name_csv.update_csv_file_on_disk() + character_name_file.update_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) + var all_translation_files: Array = ProjectSettings.get_setting('internationalization/locale/translations', []) + var added_translation_files := 0 + var removed_translation_files := 0 - if file.ends_with('.translation'): + var save_location: SaveLocationModes = ProjectSettings.get_setting('dialogic/translation/file_mode', SaveLocationModes.INSIDE_TRANSLATION_FOLDER) - if not file in translation_files: - translation_files.append(file) + _collect_translation_files(".translation", translation_files) + _collect_translation_files(".po", translation_files) - var all_translation_files: Array = ProjectSettings.get_setting('internationalization/locale/translations', []) - var orig_file_amount := len(all_translation_files) + for file_path: String in translation_files: + if not file_path in all_translation_files: + all_translation_files.append(file_path) + added_translation_files += 1 # 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: + for file_path: String in all_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) @@ -455,67 +437,83 @@ func collect_translations() -> void: 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 path_without_suffix := file_path.trim_suffix('.translation').trim_suffix(".po") var locale_part := path_without_suffix.split(".")[-1] _collect_locale(locale_part) - var valid_translation_files := PackedStringArray(all_translation_files) + var valid_translation_files := PackedStringArray(found_file_paths) ProjectSettings.set_setting('internationalization/locale/translations', valid_translation_files) ProjectSettings.save() %StatusMessage.text = ( - "Added translation files: " + str(len(all_translation_files)-orig_file_amount) + "Added translation files: " + str(added_translation_files) + "\nRemoved translation files: " + str(removed_translation_files) - + "\nTotal translation files: " + str(len(all_translation_files))) + + "\nTotal translation files: " + str(len(valid_translation_files))) -func _on_erase_translations_pressed() -> void: - %EraseConfirmationDialog.popup_centered() +func _collect_translation_files(extension: String, translation_files: Array) -> void: + for path: String in DialogicResourceUtil.list_resources_of_type(extension): + # Handle Dialogic files only. + if _is_dialogic_file(path): + translation_files.append(path) -## 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 +func _is_dialogic_file(path: String) -> bool: + var path_parts: PackedStringArray = path.split("/") + var file_name: String = path_parts[-1] - 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] + # Some file types have two dots after the base name. + var dots_after_base_name = 1 + if file_name.ends_with(".translation") or file_name.ends_with(".po") or file_name.ends_with(".import"): + dots_after_base_name = 2 - if translation_name.begins_with(csv_name): + if file_name.begins_with("dialogic_"): + return true + # Special case for timelines in PER_TIMELINE + NEXT_TO_TIMELINE mode: + elif FileAccess.file_exists(path.rsplit(".", true, dots_after_base_name)[0].trim_suffix("_translation") + ".dtl"): + return true + else: + return false - 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) +func _on_erase_translations_pressed() -> void: + %EraseConfirmationDialog.popup_centered() - 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]") +## Delete all files starting with dialogic_ and ending in [param extension]. +## [param dots_after_base_name] is the number of dots to remove from the full name +## to get the base name. +func _delete_files(extension: String, translation_files: Array) -> int: + var deleted_files: int = 0 + for path: String in DialogicResourceUtil.list_resources_of_type(extension): + # Handle Dialogic files only. + if not _is_dialogic_file(path): + continue + + # Delete the file. + if OK == DirAccess.remove_absolute(path): + var idx = translation_files.find(path) + if idx >= 0: + translation_files.remove_at(idx) + + deleted_files += 1 + print_rich("[color=green]Deleted file: " + path + "[/color]") + else: + print_rich("[color=yellow]Failed to delete file: " + path + "[/color]") return deleted_files -## Iterates over all timelines and deletes their CSVs and timeline +## Iterates over all timelines and deletes their translation files and timeline ## translation IDs. -## Deletes the Per-Project CSV file and the character name CSV file. +## Deletes the Per-Project translation file and the character name translation 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_files := 0 var deleted_translation_files := 0 var cleaned_timelines := 0 var cleaned_characters := 0 @@ -524,23 +522,14 @@ func erase_translations() -> void: 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]") + # Delete main translation files. + deleted_files += _delete_files(".csv", translation_files) + deleted_files += _delete_files(".pot", translation_files) - deleted_translation_files += delete_translations_files(translation_files, csv_name) - else: - print_rich("[color=yellow]Failed to delete CSV file: " + csv_path + "[/color]") + # Delete generated translation files. + deleted_translation_files += _delete_files(".csv.import", translation_files) + deleted_translation_files += _delete_files(".translation", translation_files) + deleted_translation_files += _delete_files(".po", translation_files) # Clean timelines. for timeline_path: String in DialogicResourceUtil.list_resources_of_type(".dtl"): @@ -583,7 +572,7 @@ func erase_translations() -> void: Characters cleaned {cleaned_characters} Glossaries cleaned {cleaned_glossaries} - CSVs erased {erased_csv_files} + Files erased {erased_files} Translations erased {erased_translation_files}" var status_message_args := { @@ -591,7 +580,7 @@ func erase_translations() -> void: 'cleaned_characters': cleaned_characters, 'cleaned_events': cleaned_events, 'cleaned_glossaries': cleaned_glossaries, - 'erased_csv_files': deleted_csv_files, + 'erased_files': deleted_files, 'erased_translation_files': deleted_translation_files, } @@ -603,6 +592,7 @@ func erase_translations() -> void: # Clear the internal settings. ProjectSettings.clear('dialogic/translation/intern/save_mode') ProjectSettings.clear('dialogic/translation/intern/file_mode') + ProjectSettings.clear('dialogic/translation/intern/file_format') ProjectSettings.clear('dialogic/translation/intern/translation_folder') _verify_translation_file() @@ -658,3 +648,15 @@ func _collect_locale(locale: String) -> void: return _unique_locales.append(locale) + + +func _open_translation_file(path: String, orig_locale: String) -> DialogicTranslationFile: + match ProjectSettings.get_setting('dialogic/translation/file_format', FileFormat.CSV): + FileFormat.CSV: + var add_separator_lines: bool = ProjectSettings.get_setting('dialogic/translation/add_separator', false) + return DialogicTranslationCsvFile.new(path + ".csv", orig_locale, add_separator_lines) + FileFormat.GETTEXT: + return DialogicTranslationGettextFile.new(path + ".pot", orig_locale) + _: + assert(false, "Invalid FileFormat") + return null diff --git a/addons/dialogic/Editor/Settings/settings_translation.tscn b/addons/dialogic/Editor/Settings/settings_translation.tscn index 61fca18fd..3ae132728 100644 --- a/addons/dialogic/Editor/Settings/settings_translation.tscn +++ b/addons/dialogic/Editor/Settings/settings_translation.tscn @@ -1,11 +1,11 @@ -[gd_scene load_steps=7 format=3 uid="uid://chpb1mj03xjxv"] +[gd_scene load_steps=9 format=3 uid="uid://chpb1mj03xjxv"] [ext_resource type="Script" path="res://addons/dialogic/Editor/Settings/settings_translation.gd" id="1_dvmyi"] [ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_k2lou"] [ext_resource type="PackedScene" uid="uid://dpwhshre1n4t6" path="res://addons/dialogic/Editor/Events/Fields/field_options_dynamic.tscn" id="3_dq4j2"] [ext_resource type="PackedScene" uid="uid://7mvxuaulctcq" path="res://addons/dialogic/Editor/Events/Fields/field_file.tscn" id="4_kvsma"] -[sub_resource type="Image" id="Image_g2hic"] +[sub_resource type="Image" id="Image_qkysl"] 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,20 @@ data = { "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_xbph7"] -image = SubResource("Image_g2hic") +[sub_resource type="ImageTexture" id="ImageTexture_ix87w"] +image = SubResource("Image_qkysl") + +[sub_resource type="Image" id="Image_t7nt4"] +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", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_2886t"] +image = SubResource("Image_t7nt4") [node name="Translations" type="VBoxContainer"] anchors_preset = 15 @@ -72,8 +84,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). -Equivalent of the testing local project setting. " -texture = SubResource("ImageTexture_xbph7") +Equivalent of the testing local project setting. + +Update dropdown list via \"Collect Translation\"." +texture = SubResource("ImageTexture_ix87w") hint_text = "Change this locale to test your game in a different language (only in-editor). Equivalent of the testing local project setting. @@ -113,7 +127,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_xbph7") +texture = SubResource("ImageTexture_ix87w") hint_text = "The locale of the language your timelines are written in." [node name="OrigLocale" parent="TranslationSettings/VBoxContainer/Grid" instance=ExtResource("3_dq4j2")] @@ -129,10 +143,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. +tooltip_text = "Choose a folder to let Dialogic save translation files in. Also used when saving \"Inside Translation Folder\"" -texture = SubResource("ImageTexture_xbph7") -hint_text = "Choose a folder to let Dialogic save CSV files in. +texture = SubResource("ImageTexture_ix87w") +hint_text = "Choose a folder to let Dialogic save translation files in. Also used when saving \"Inside Translation Folder\"" [node name="TransFolderPicker" parent="TranslationSettings/VBoxContainer/Grid" instance=ExtResource("4_kvsma")] @@ -141,6 +155,37 @@ layout_mode = 2 size_flags_horizontal = 3 file_mode = 2 +[node name="VBox3" type="HBoxContainer" parent="TranslationSettings/VBoxContainer/Grid"] +layout_mode = 2 + +[node name="FileFormatLabel" type="Label" parent="TranslationSettings/VBoxContainer/Grid/VBox3"] +layout_mode = 2 +text = "File Format" + +[node name="FileFormatTooltip" parent="TranslationSettings/VBoxContainer/Grid/VBox3" instance=ExtResource("2_k2lou")] +layout_mode = 2 +tooltip_text = "Decides which format is used for the generated translation files. + +• \"CSV\": Uses a single .csv file for all languages. It can be edited with any spreadsheet editor. + +• \"gettext\": Uses one .po file per language plus a .pot file as template. It much easier to import into translation platforms." +texture = SubResource("ImageTexture_ix87w") +hint_text = "Decides which format is used for the generated translation files. + +• \"CSV\": Uses a single .csv file for all languages. It can be edited with any spreadsheet editor. + +• \"gettext\": Uses one .po file per language plus a .pot file as template. It much easier to import into translation platforms." + +[node name="FileFormat" type="OptionButton" parent="TranslationSettings/VBoxContainer/Grid"] +unique_name_in_owner = true +layout_mode = 2 +item_count = 2 +selected = 0 +popup/item_0/text = "CSV" +popup/item_0/id = 0 +popup/item_1/text = "gettext" +popup/item_1/id = 1 + [node name="VBox2" type="HBoxContainer" parent="TranslationSettings/VBoxContainer/Grid"] layout_mode = 2 @@ -150,23 +195,23 @@ text = "Output mode" [node name="OutputModeTooltip" parent="TranslationSettings/VBoxContainer/Grid/VBox2" instance=ExtResource("2_k2lou")] layout_mode = 2 -tooltip_text = "Decides how many CSV files will be created. +tooltip_text = "Decides how many translation files will be created. -• \"Per Type\": Uses one CSV file for each type of resource: Timelines, characters, and glossaries. -For example, 10 timelines will be combined into 1 CSV file. +• \"Per Type\": Uses one translation file for each type of resource: Timelines, characters, and glossaries. +For example, 10 timelines will be combined into 1 translation file. -• \"Per File\": Uses one CSV file for each resource file. -For example, 10 timelines will result in 10 CSV files. +• \"Per File\": Uses one translation file for each resource file. +For example, 10 timelines will result in 10 translation files. The \"Per File\" option utilises \"Output location\", in contrast, the \"Per Type\" will always use the Translation folder." -texture = SubResource("ImageTexture_xbph7") -hint_text = "Decides how many CSV files will be created. +texture = SubResource("ImageTexture_ix87w") +hint_text = "Decides how many translation files will be created. -• \"Per Type\": Uses one CSV file for each type of resource: Timelines, characters, and glossaries. -For example, 10 timelines will be combined into 1 CSV file. +• \"Per Type\": Uses one translation file for each type of resource: Timelines, characters, and glossaries. +For example, 10 timelines will be combined into 1 translation file. -• \"Per File\": Uses one CSV file for each resource file. -For example, 10 timelines will result in 10 CSV files. +• \"Per File\": Uses one translation file for each resource file. +For example, 10 timelines will result in 10 translation files. The \"Per File\" option utilises \"Output location\", in contrast, the \"Per Type\" will always use the Translation folder." @@ -189,7 +234,7 @@ text = "Output location" [node name="OutputLocationTooltip" parent="TranslationSettings/VBoxContainer/Grid/OutputLocation" instance=ExtResource("2_k2lou")] layout_mode = 2 -tooltip_text = "Decides where to save the generated CSV files. +tooltip_text = "Decides where to save the generated translation files. • \"Inside Translation Folder\": Uses the \"Translation folder\". @@ -197,8 +242,8 @@ tooltip_text = "Decides where to save the generated CSV files. This button requires the \"Per File\" Output mode. A resource type can be: Timelines, characters, and glossaries." -texture = SubResource("ImageTexture_xbph7") -hint_text = "Decides where to save the generated CSV files. +texture = SubResource("ImageTexture_ix87w") +hint_text = "Decides where to save the generated translation files. • \"Inside Translation Folder\": Uses the \"Translation folder\". @@ -234,7 +279,7 @@ layout_mode = 2 tooltip_text = "Adds an empty line into per-project CSVs to differentiate between sections. For example, when a new glossary item or timeline starts, an empty line will be added." -texture = SubResource("ImageTexture_xbph7") +texture = SubResource("ImageTexture_ix87w") hint_text = "Adds an empty line into per-project CSVs to differentiate between sections. For example, when a new glossary item or timeline starts, an empty line will be added." @@ -262,24 +307,24 @@ text = "Actions" layout_mode = 2 columns = 2 -[node name="UpdateCsvFiles" type="Button" parent="TranslationSettings/VBoxContainer2/Actions"] +[node name="UpdateTranslationFiles" type="Button" parent="TranslationSettings/VBoxContainer2/Actions"] unique_name_in_owner = true layout_mode = 2 disabled = true -text = "Update CSV files" -icon = SubResource("ImageTexture_xbph7") +text = "Update translation files" +icon = SubResource("ImageTexture_2886t") [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. +tooltip_text = "This button will scan all timelines and generate or update their translation files. -A Dialogic CSV file will be prefixed with \"dialogic_\". +A Dialogic translation file will be prefixed with \"dialogic_\". This action will be disabled if the \"Translation folder\" is missing or has an invalid path." -texture = SubResource("ImageTexture_xbph7") -hint_text = "This button will scan all timelines and generate or update their CSV files. +texture = SubResource("ImageTexture_ix87w") +hint_text = "This button will scan all timelines and generate or update their translation files. -A Dialogic CSV file will be prefixed with \"dialogic_\". +A Dialogic translation file will be prefixed with \"dialogic_\". This action will be disabled if the \"Translation folder\" is missing or has an invalid path." @@ -287,16 +332,16 @@ This action will be disabled if the \"Translation folder\" is missing or has an unique_name_in_owner = true layout_mode = 2 text = "Collect translations" -icon = SubResource("ImageTexture_xbph7") +icon = SubResource("ImageTexture_2886t") [node name="HintTooltip6" parent="TranslationSettings/VBoxContainer2/Actions" instance=ExtResource("2_k2lou")] layout_mode = 2 -tooltip_text = "Godot imports CSV files as \".translation\" files. -This buttons adds them to \"Project Settings -> Localization\". +tooltip_text = "This buttons adds the translation files to \"Project Settings -> Localization\". +Godot imports CSV files as \".translation\" files so they are added instead of the CSV files. " -texture = SubResource("ImageTexture_xbph7") -hint_text = "Godot imports CSV files as \".translation\" files. -This buttons adds them to \"Project Settings -> Localization\". +texture = SubResource("ImageTexture_ix87w") +hint_text = "This buttons adds the translation files to \"Project Settings -> Localization\". +Godot imports CSV files as \".translation\" files so they are added instead of the CSV files. " [node name="AspectRatioContainer2" type="AspectRatioContainer" parent="TranslationSettings/VBoxContainer2/Actions"] @@ -311,28 +356,28 @@ layout_mode = 2 unique_name_in_owner = true layout_mode = 2 text = "Remove translations" -icon = SubResource("ImageTexture_xbph7") +icon = SubResource("ImageTexture_2886t") [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. -CSV and translation files prefixed with \"dialogic_\" are treated as Dialogic-related. +It will try to delete any \".csv\", \".pot\", \".translation\" and \".po\" files that are related to Dialogic. +Translation files prefixed with \"dialogic_\" are treated as Dialogic-related. Removes translation IDs (eg. #id:33) from timelines and characters." -texture = SubResource("ImageTexture_xbph7") +texture = SubResource("ImageTexture_ix87w") hint_text = "Be very careful with this button! -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. +It will try to delete any \".csv\", \".pot\", \".translation\" and \".po\" files that are related to Dialogic. +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 = "⛔ Requires valid translation folder to translate character names and the project CSV file." +text = "⛔ Requires valid translation folder to translate character names and the translation file." autowrap_mode = 3 [node name="UpdateConfirmationDialog" type="ConfirmationDialog" parent="."] @@ -340,9 +385,9 @@ 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! +dialog_text = "You have previously generated translation files with different Translation Settings! -Please consider to delete the old CSVs and then generate new changes." +Please consider to delete the old files and then generate new changes." dialog_autowrap = true [node name="EraseConfirmationDialog" type="ConfirmationDialog" parent="."] @@ -352,9 +397,7 @@ 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. +- Delete all translation files prefixed with \"dialogic_\" including their import files. - Remove translation IDs from timelines and characters. - Remove all \"dialogic\" prefixed translations from \"Project Settings -> Localization\". - Remove the \"_translation_keys\" and \"entries\" starting with \"Glossary/\"." diff --git a/addons/dialogic/Modules/Glossary/glossary_resource.gd b/addons/dialogic/Modules/Glossary/glossary_resource.gd index 638d03697..5365fdd04 100644 --- a/addons/dialogic/Modules/Glossary/glossary_resource.gd +++ b/addons/dialogic/Modules/Glossary/glossary_resource.gd @@ -15,10 +15,10 @@ extends Resource ## If false, no entries from this glossary will be shown @export var enabled: bool = true -## Refers to the translation type of this resource used for CSV translation files. +## Refers to the translation type of this resource used for translation files. const RESOURCE_NAME := "Glossary" ## The name of glossary entries, the value is the key in [member entries]. -## This constant is used for CSV translation files. +## This constant is used for translation files. const NAME_PROPERTY := "name" ## Property in a glossary entry. Alternative words for the entry name. const ALTERNATIVE_PROPERTY := "alternatives" @@ -41,13 +41,6 @@ const PRIVATE_PROPERTY_PREFIX := "_" ## Private ID assigned when this glossary is translated. @export var _translation_id := "" -## Private lookup table used to find the translation ID of a glossary entry. -## The keys (String) are all translated words that may trigger a glossary entry to -## be shown. -## The values (String) are the translation ID. -@export var _translation_keys := {} - - ## Removes an entry and all its aliases (alternative property) from ## the glossary. @@ -256,14 +249,12 @@ func clear_translation_keys() -> void: if translation_key.begins_with(RESOURCE_NAME_KEY): entries.erase(translation_key) - _translation_keys.clear() - #endregion #region GET AND SET TRANSLATION IDS AND KEYS -## Returns a key used to reference this glossary in the translation CSV file. +## Returns a key used to reference this glossary in the translation file. ## ## Time complexity: O(1) func get_property_translation_key(entry_key: String, property: String) -> String: @@ -277,12 +268,12 @@ func get_property_translation_key(entry_key: String, property: String) -> String if entry_translation_key.is_empty() or _translation_id.is_empty(): return "" - var glossary_csv_key := (RESOURCE_NAME + var glossary_translation_key := (RESOURCE_NAME .path_join(_translation_id) .path_join(entry_translation_key) .path_join(property)) - return glossary_csv_key + return glossary_translation_key diff --git a/addons/dialogic/Modules/Glossary/subsystem_glossary.gd b/addons/dialogic/Modules/Glossary/subsystem_glossary.gd index d438bc4bc..1b142959d 100644 --- a/addons/dialogic/Modules/Glossary/subsystem_glossary.gd +++ b/addons/dialogic/Modules/Glossary/subsystem_glossary.gd @@ -138,17 +138,9 @@ func get_entry(entry_key: String) -> Dictionary: result.color = ProjectSettings.get_setting(SETTING_DEFAULT_COLOR, Color.POWDER_BLUE) if is_translation_enabled and not glossary._translation_id.is_empty(): - var translation_key: String = glossary._translation_keys.get(entry_key) - var last_slash := translation_key.rfind('/') - - if last_slash == -1: - return {} - - var tr_base := translation_key.substr(0, last_slash) - - result.title = translate(tr_base, "title", entry) - result.text = translate(tr_base, "text", entry) - result.extra = translate(tr_base, "extra", entry) + result.title = translate(glossary, entry, "title") + result.text = translate(glossary, entry, "text") + result.extra = translate(glossary, entry, "extra") else: result.title = entry.get("title", "") result.text = entry.get("text", "") @@ -164,11 +156,11 @@ func get_entry(entry_key: String) -> Dictionary: ## Tries to translate the property with the given -func translate(tr_base: String, property: StringName, fallback_entry: Dictionary) -> String: - var tr_key := tr_base.path_join(property) +func translate(glossary: DialogicGlossary, entry: Dictionary, property: StringName) -> String: + var tr_key := glossary._get_glossary_translation_key(entry[DialogicGlossary.TRANSLATION_PROPERTY], property) var tr_value := tr(tr_key) if tr_key == tr_value: - tr_value = fallback_entry.get(property, "") + tr_value = entry.get(property, "") return tr_value diff --git a/addons/dialogic/Resources/event.gd b/addons/dialogic/Resources/event.gd index 68f78360a..f093d1db1 100644 --- a/addons/dialogic/Resources/event.gd +++ b/addons/dialogic/Resources/event.gd @@ -47,6 +47,10 @@ var event_node_as_text := "" var event_node_ready := false ## How many empty lines are before this event var empty_lines_above: int = 0 +## Path to the resource file the event has been loaded from. +var source_path: String = "" +## Line number inside the resource file. +var source_line_number: int = -1 ### Editor UI Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/addons/dialogic/Resources/timeline.gd b/addons/dialogic/Resources/timeline.gd index 651053cc6..623b89307 100644 --- a/addons/dialogic/Resources/timeline.gd +++ b/addons/dialogic/Resources/timeline.gd @@ -118,6 +118,8 @@ func process() -> void: event = i.duplicate() break + event.source_path = resource_path + event.source_line_number = idx + 1 event.empty_lines_above = empty_lines # add the following lines until the event says it's full or there is an empty line while !event.is_string_full_event(event_content):