]> Untitled Git - wolf-seeking-sheep.git/blob - addons/dialogic/Editor/Settings/csv_file.gd
Updated export config options
[wolf-seeking-sheep.git] / addons / dialogic / Editor / Settings / csv_file.gd
1 class_name DialogicCsvFile
2 extends RefCounted
3 ## Handles translation of a [class DialogicTimeline] to a CSV file.
4
5 var lines: Array[PackedStringArray] = []
6 ## Dictionary of lines from the original file.
7 ## Key: String, Value: PackedStringArray
8 var old_lines: Dictionary = {}
9
10 ## The amount of columns the CSV file has after loading it.
11 ## Used to add trailing commas to new lines.
12 var column_count := 0
13
14 ## Whether this CSV file was able to be loaded a defined
15 ## file path.
16 var is_new_file: bool = false
17
18 ## The underlying file used to read and write the CSV file.
19 var file: FileAccess
20
21 ## File path used to load the CSV file.
22 var used_file_path: String
23
24 ## The amount of events that were updated in the CSV file.
25 var updated_rows: int = 0
26
27 ## The amount of events that were added to the CSV file.
28 var new_rows: int = 0
29
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
34
35 enum PropertyType {
36         String = 0,
37         Array = 1,
38         Other = 2,
39 }
40
41 ## The translation property used for the glossary item translation.
42 const TRANSLATION_ID := DialogicGlossary.TRANSLATION_PROPERTY
43
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]
46 ## array.
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
52
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)
57
58         if not ResourceLoader.exists(file_path):
59                 is_new_file = true
60
61                 # The "keys" and original locale are the only columns in a new file.
62                 # For example: "keys, en"
63                 column_count = 2
64                 return
65
66         file = FileAccess.open(file_path, FileAccess.READ)
67
68         var locale_csv_row := file.get_csv_line()
69         column_count = locale_csv_row.size()
70         var locale_key := locale_csv_row[0]
71
72         old_lines[locale_key] = locale_csv_row
73
74         _read_file_into_lines()
75
76
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]
83
84                 old_lines[row_key] = line
85
86
87 ## Collects names from the given [param characters] and adds them to the
88 ## [member lines].
89 ##
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)
100
101                 var nicknames: Array = character.nicknames
102
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)
109
110                 # New character item, if needed, add a separator.
111                 if add_separator:
112                         _append_empty()
113
114
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)
119
120
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
125
126         if key.ends_with(DialogicGlossary.ALTERNATIVE_PROPERTY):
127                 return PropertyType.Array
128
129         return PropertyType.Other
130
131
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] = []
135
136         for i in csv_values.size():
137
138                 if i == KEY_VALUE_INDEX:
139                         continue
140
141                 var csv_value := csv_values[i]
142
143                 if csv_value.is_empty():
144                         continue
145
146                 match property_type:
147                         PropertyType.String:
148                                 values_as_array = [csv_value]
149
150                         PropertyType.Array:
151                                 var split_values := csv_value.split(",")
152
153                                 for value in split_values:
154                                         values_as_array.append(value)
155
156         return values_as_array
157
158
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)
162
163         for glossary_line: PackedStringArray in names:
164
165                 if glossary_line.is_empty():
166                         continue
167
168                 var csv_key := glossary_line[0]
169
170                 # CSV line separators will be empty.
171                 if not csv_key.begins_with(glossary_prefix_key):
172                         continue
173
174                 var value_type := _get_key_type(csv_key)
175
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)):
179                         continue
180
181                 var new_line_to_add := _process_line_into_array(glossary_line, value_type)
182
183                 for name_to_add: String in new_line_to_add:
184                         glossary._translation_keys[name_to_add.strip_edges()] = csv_key
185
186
187
188 ## Reads all [member lines] and adds them to the given [param glossary]'s
189 ## internal collection of words-to-translation-key mappings.
190 ##
191 ## Populate the CSV's lines with the method [method collect_lines_from_glossary]
192 ## before.
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())
197
198
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:
203         return (
204                 DialogicGlossary.RESOURCE_NAME
205                         .path_join(glossary._translation_id)
206         )
207
208
209 ## Returns whether [param value_b] is greater than [param value_a].
210 ##
211 ## This method helps to sort glossary entry properties by their importance
212 ## matching the order in the editor.
213 ##
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,
221         }
222         const UNKNOWN_PROPERTY_ORDER := 100
223
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)
226
227         return value_a < value_b
228
229
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:
233
234         for glossary_value: Variant in glossary.entries.values():
235
236                 if glossary_value is String:
237                         continue
238
239                 var glossary_entry: Dictionary = glossary_value
240                 var glossary_entry_name: String = glossary_entry[DialogicGlossary.NAME_PROPERTY]
241
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)
244
245                 var entry_property_keys := glossary_entry.keys().duplicate()
246                 entry_property_keys.sort_custom(_sort_glossary_entry_property_keys)
247
248                 var entry_name_property: String = glossary_entry[DialogicGlossary.NAME_PROPERTY]
249
250                 for entry_key: String in entry_property_keys:
251                         # Ignore private keys.
252                         if entry_key.begins_with(DialogicGlossary.PRIVATE_PROPERTY_PREFIX):
253                                 continue
254
255                         var item_value: Variant = glossary_entry[entry_key]
256                         var item_value_str := ""
257
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)
262
263                         elif not item_value is String or item_value.is_empty():
264                                 continue
265
266                         else:
267                                 item_value_str = item_value
268
269                         var glossary_csv_key := glossary._get_glossary_translation_key(entry_translation_id, entry_key)
270
271                         if (entry_key == DialogicGlossary.NAME_PROPERTY
272                         or entry_key == DialogicGlossary.ALTERNATIVE_PROPERTY):
273                                 glossary.entries[glossary_csv_key] = entry_name_property
274
275                         var glossary_line := PackedStringArray([glossary_csv_key, item_value_str])
276
277                         lines.append(glossary_line)
278
279                 # New glossary item, if needed, add a separator.
280                 if add_separator:
281                         _append_empty()
282
283
284
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:
289
290                 if event.can_be_translated():
291
292                         if event._translation_id.is_empty():
293                                 event.add_translation_id()
294                                 event.update_text_version()
295
296                         var properties: Array = event._get_translatable_properties()
297
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)
303
304         # End of timeline, if needed, add a separator.
305         if add_separator:
306                 _append_empty()
307
308
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.
313 ##
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.
318         if lines.size() < 2:
319                 print_rich("[color=yellow]No lines for the CSV file, skipping: " + used_file_path)
320
321                 return
322
323         # Clear the current CSV file.
324         file = FileAccess.open(used_file_path, FileAccess.WRITE)
325
326         for line in lines:
327                 var row_key := line[0]
328
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)
334
335                         var line_columns: int = updated_line.size()
336                         var line_columns_to_add := column_count - line_columns
337
338                         # Add trailing commas to match the amount of columns.
339                         for _i in range(line_columns_to_add):
340                                 updated_line.append("")
341
342                         file.store_csv_line(updated_line)
343                         updated_rows += 1
344
345                 else:
346                         var line_columns: int = line.size()
347                         var line_columns_to_add := column_count - line_columns
348
349                         # Add trailing commas to match the amount of columns.
350                         for _i in range(line_columns_to_add):
351                                 line.append("")
352
353                         file.store_csv_line(line)
354                         new_rows += 1
355
356         file.close()