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

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()