1 class_name DialogicCsvFile
3 ## Handles translation of a [class DialogicTimeline] to a CSV file.
5 var lines: Array[PackedStringArray] = []
6 ## Dictionary of lines from the original file.
7 ## Key: String, Value: PackedStringArray
8 var old_lines: Dictionary = {}
10 ## The amount of columns the CSV file has after loading it.
11 ## Used to add trailing commas to new lines.
14 ## Whether this CSV file was able to be loaded a defined
16 var is_new_file: bool = false
18 ## The underlying file used to read and write the CSV file.
21 ## File path used to load the CSV file.
22 var used_file_path: String
24 ## The amount of events that were updated in the CSV file.
25 var updated_rows: int = 0
27 ## The amount of events that were added to the CSV file.
30 ## Whether this CSV handler should add newlines as a separator between sections.
31 ## A section may be a new character, new timeline, or new glossary item inside
32 ## a per-project file.
33 var add_separator: bool = false
41 ## The translation property used for the glossary item translation.
42 const TRANSLATION_ID := DialogicGlossary.TRANSLATION_PROPERTY
44 ## Attempts to load the CSV file from [param file_path].
45 ## If the file does not exist, a single entry is added to the [member lines]
47 ## The [param separator_enabled] enables adding newlines as a separator to
48 ## per-project files. This is useful for readability.
49 func _init(file_path: String, original_locale: String, separator_enabled: bool) -> void:
50 used_file_path = file_path
51 add_separator = separator_enabled
53 # The first entry must be the locale row.
54 # [method collect_lines_from_timeline] will add the other locales, if any.
55 var locale_array_line := PackedStringArray(["keys", original_locale])
56 lines.append(locale_array_line)
58 if not ResourceLoader.exists(file_path):
61 # The "keys" and original locale are the only columns in a new file.
62 # For example: "keys, en"
66 file = FileAccess.open(file_path, FileAccess.READ)
68 var locale_csv_row := file.get_csv_line()
69 column_count = locale_csv_row.size()
70 var locale_key := locale_csv_row[0]
72 old_lines[locale_key] = locale_csv_row
74 _read_file_into_lines()
77 ## Private function to read the CSV file into the [member lines] array.
78 ## Cannot be called on a new file.
79 func _read_file_into_lines() -> void:
80 while not file.eof_reached():
81 var line := file.get_csv_line()
82 var row_key := line[0]
84 old_lines[row_key] = line
87 ## Collects names from the given [param characters] and adds them to the
90 ## If this is the character name CSV file, use this method to
91 ## take previously collected characters from other [class DialogicCsvFile]s.
92 func collect_lines_from_characters(characters: Dictionary) -> void:
93 for character: DialogicCharacter in characters.values():
94 # Add row for display names.
95 var name_property := DialogicCharacter.TranslatedProperties.NAME
96 var display_name_key: String = character.get_property_translation_key(name_property)
97 var line_value: String = character.display_name
98 var array_line := PackedStringArray([display_name_key, line_value])
99 lines.append(array_line)
101 var nicknames: Array = character.nicknames
103 if not nicknames.is_empty():
104 var nick_name_property := DialogicCharacter.TranslatedProperties.NICKNAMES
105 var nickname_string: String = ",".join(nicknames)
106 var nickname_name_line_key: String = character.get_property_translation_key(nick_name_property)
107 var nick_array_line := PackedStringArray([nickname_name_line_key, nickname_string])
108 lines.append(nick_array_line)
110 # New character item, if needed, add a separator.
115 ## Appends an empty line to the [member lines] array.
116 func _append_empty() -> void:
117 var empty_line := PackedStringArray(["", ""])
118 lines.append(empty_line)
121 ## Returns the property type for the given [param key].
122 func _get_key_type(key: String) -> PropertyType:
123 if key.ends_with(DialogicGlossary.NAME_PROPERTY):
124 return PropertyType.String
126 if key.ends_with(DialogicGlossary.ALTERNATIVE_PROPERTY):
127 return PropertyType.Array
129 return PropertyType.Other
132 func _process_line_into_array(csv_values: PackedStringArray, property_type: PropertyType) -> Array[String]:
133 const KEY_VALUE_INDEX := 0
134 var values_as_array: Array[String] = []
136 for i in csv_values.size():
138 if i == KEY_VALUE_INDEX:
141 var csv_value := csv_values[i]
143 if csv_value.is_empty():
148 values_as_array = [csv_value]
151 var split_values := csv_value.split(",")
153 for value in split_values:
154 values_as_array.append(value)
156 return values_as_array
159 func _add_keys_to_glossary(glossary: DialogicGlossary, names: Array) -> void:
160 var glossary_prefix_key := glossary._get_glossary_translation_id_prefix()
161 var glossary_translation_id_prefix := _get_glossary_translation_key_prefix(glossary)
163 for glossary_line: PackedStringArray in names:
165 if glossary_line.is_empty():
168 var csv_key := glossary_line[0]
170 # CSV line separators will be empty.
171 if not csv_key.begins_with(glossary_prefix_key):
174 var value_type := _get_key_type(csv_key)
176 # String and Array are the only valid types.
177 if (value_type == PropertyType.Other
178 or not csv_key.begins_with(glossary_translation_id_prefix)):
181 var new_line_to_add := _process_line_into_array(glossary_line, value_type)
183 for name_to_add: String in new_line_to_add:
184 glossary._translation_keys[name_to_add.strip_edges()] = csv_key
188 ## Reads all [member lines] and adds them to the given [param glossary]'s
189 ## internal collection of words-to-translation-key mappings.
191 ## Populate the CSV's lines with the method [method collect_lines_from_glossary]
193 func add_translation_keys_to_glossary(glossary: DialogicGlossary) -> void:
194 glossary._translation_keys.clear()
195 _add_keys_to_glossary(glossary, lines)
196 _add_keys_to_glossary(glossary, old_lines.values())
199 ## Returns the translation key prefix for the given [param glossary_translation_id].
200 ## The resulting format will look like this: Glossary/a2/
201 ## You can use this to find entries in [member lines] that to a glossary.
202 func _get_glossary_translation_key_prefix(glossary: DialogicGlossary) -> String:
204 DialogicGlossary.RESOURCE_NAME
205 .path_join(glossary._translation_id)
209 ## Returns whether [param value_b] is greater than [param value_a].
211 ## This method helps to sort glossary entry properties by their importance
212 ## matching the order in the editor.
214 ## TODO: Allow Dialogic users to define their own order.
215 func _sort_glossary_entry_property_keys(property_key_a: String, property_key_b: String) -> bool:
216 const GLOSSARY_CSV_LINE_ORDER := {
217 DialogicGlossary.NAME_PROPERTY: 0,
218 DialogicGlossary.ALTERNATIVE_PROPERTY: 1,
219 DialogicGlossary.TEXT_PROPERTY: 2,
220 DialogicGlossary.EXTRA_PROPERTY: 3,
222 const UNKNOWN_PROPERTY_ORDER := 100
224 var value_a: int = GLOSSARY_CSV_LINE_ORDER.get(property_key_a, UNKNOWN_PROPERTY_ORDER)
225 var value_b: int = GLOSSARY_CSV_LINE_ORDER.get(property_key_b, UNKNOWN_PROPERTY_ORDER)
227 return value_a < value_b
230 ## Collects properties from glossary entries from the given [param glossary] and
231 ## adds them to the [member lines].
232 func collect_lines_from_glossary(glossary: DialogicGlossary) -> void:
234 for glossary_value: Variant in glossary.entries.values():
236 if glossary_value is String:
239 var glossary_entry: Dictionary = glossary_value
240 var glossary_entry_name: String = glossary_entry[DialogicGlossary.NAME_PROPERTY]
242 var _glossary_translation_id := glossary.get_set_glossary_translation_id()
243 var entry_translation_id := glossary.get_set_glossary_entry_translation_id(glossary_entry_name)
245 var entry_property_keys := glossary_entry.keys().duplicate()
246 entry_property_keys.sort_custom(_sort_glossary_entry_property_keys)
248 var entry_name_property: String = glossary_entry[DialogicGlossary.NAME_PROPERTY]
250 for entry_key: String in entry_property_keys:
251 # Ignore private keys.
252 if entry_key.begins_with(DialogicGlossary.PRIVATE_PROPERTY_PREFIX):
255 var item_value: Variant = glossary_entry[entry_key]
256 var item_value_str := ""
258 if item_value is Array:
259 var item_array := item_value as Array
260 # We use a space after the comma to make it easier to read.
261 item_value_str = " ,".join(item_array)
263 elif not item_value is String or item_value.is_empty():
267 item_value_str = item_value
269 var glossary_csv_key := glossary._get_glossary_translation_key(entry_translation_id, entry_key)
271 if (entry_key == DialogicGlossary.NAME_PROPERTY
272 or entry_key == DialogicGlossary.ALTERNATIVE_PROPERTY):
273 glossary.entries[glossary_csv_key] = entry_name_property
275 var glossary_line := PackedStringArray([glossary_csv_key, item_value_str])
277 lines.append(glossary_line)
279 # New glossary item, if needed, add a separator.
285 ## Collects translatable events from the given [param timeline] and adds
286 ## them to the [member lines].
287 func collect_lines_from_timeline(timeline: DialogicTimeline) -> void:
288 for event: DialogicEvent in timeline.events:
290 if event.can_be_translated():
292 if event._translation_id.is_empty():
293 event.add_translation_id()
294 event.update_text_version()
296 var properties: Array = event._get_translatable_properties()
298 for property: String in properties:
299 var line_key: String = event.get_property_translation_key(property)
300 var line_value: String = event._get_property_original_translation(property)
301 var array_line := PackedStringArray([line_key, line_value])
302 lines.append(array_line)
304 # End of timeline, if needed, add a separator.
309 ## Clears the CSV file on disk and writes the current [member lines] array to it.
310 ## Uses the [member old_lines] dictionary to update existing translations.
311 ## If a translation row misses a column, a trailing comma will be added to
312 ## conform to the CSV file format.
314 ## If the locale CSV line was collected only, a new file won't be created and
315 ## already existing translations won't be updated.
316 func update_csv_file_on_disk() -> void:
317 # None or locale row only.
319 print_rich("[color=yellow]No lines for the CSV file, skipping: " + used_file_path)
323 # Clear the current CSV file.
324 file = FileAccess.open(used_file_path, FileAccess.WRITE)
327 var row_key := line[0]
329 # In case there might be translations for this line already,
330 # add them at the end again (orig locale text is replaced).
331 if row_key in old_lines:
332 var old_line: PackedStringArray = old_lines[row_key]
333 var updated_line: PackedStringArray = line + old_line.slice(2)
335 var line_columns: int = updated_line.size()
336 var line_columns_to_add := column_count - line_columns
338 # Add trailing commas to match the amount of columns.
339 for _i in range(line_columns_to_add):
340 updated_line.append("")
342 file.store_csv_line(updated_line)
346 var line_columns: int = line.size()
347 var line_columns_to_add := column_count - line_columns
349 # Add trailing commas to match the amount of columns.
350 for _i in range(line_columns_to_add):
353 file.store_csv_line(line)