You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
357 lines
11 KiB
357 lines
11 KiB
6 months ago
|
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()
|