## A collection of utility functions for working with dialogue translations. class_name DMTranslationUtilities extends RefCounted ## Generate translation keys from some text. static func generate_static_line_ids_for_project() -> void: var rng: RandomNumberGenerator = RandomNumberGenerator.new() rng.randomize() for file_path: String in DMCache.get_files(): var text: String = FileAccess.get_file_as_string(file_path) text = generate_static_line_ids_for_text(text, file_path) var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE) file.store_string(text) file.close() ## Generate static line IDs for some text. static func generate_static_line_ids_for_text(text: String, file_path: String) -> String: var lines: PackedStringArray = text.split("\n") var compiled_lines: Dictionary = DMCompiler.compile_string(text, "").lines # Add in any that are missing for i: int in lines.size(): var line: String = lines[i] var l: String = line.strip_edges() if not [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE].has(DMCompiler.get_line_type(l)): continue if not compiled_lines.has(str(i)): continue if "[ID:" in line: continue var translatable_text: String = "" if l.begins_with("- "): translatable_text = DMCompiler.extract_translatable_string(l) else: translatable_text = l.substr(l.find(":") + 1) var key: String = _generate_id(file_path) while key in DMCache.known_static_ids: key = _generate_id(file_path) line = line.replace("\\n", "!NEWLINE!") translatable_text = translatable_text.replace("\\n", "!NEWLINE!") lines[i] = line.replace(translatable_text, translatable_text + " [ID:%s]" % [key]).replace("!NEWLINE!", "\\n") DMCache.known_static_ids[key] = file_path return "\n".join(lines) ## Get a random-ish ID for a line. static func _generate_id(file_path: String) -> String: return (file_path.sha1_text().substr(0, 6) + "_" + str(randi() % 1000000).sha1_text().substr(0, 6)).to_upper() ## Export dialogue and responses to CSV. static func export_translations_to_csv(to_path: String, text: String, dialogue_path: String) -> void: var default_locale: String = DMSettings.get_setting(DMSettings.DEFAULT_CSV_LOCALE, "en") var file: FileAccess # If the file exists, open it first and work out which keys are already in it var existing_csv: Dictionary = {} var delimiter: String = get_delimiter_for_csv(to_path) var column_count: int = 2 var default_locale_column: int = 1 var character_column: int = -1 var notes_column: int = -1 if FileAccess.file_exists(to_path): file = FileAccess.open(to_path, FileAccess.READ) var is_first_line = true var line: Array while !file.eof_reached(): line = file.get_csv_line(delimiter) if is_first_line: is_first_line = false column_count = line.size() for i in range(1, line.size()): if line[i] == default_locale: default_locale_column = i elif line[i] == "_character": character_column = i elif line[i] == "_notes": notes_column = i # Make sure the line isn't empty before adding it if line.size() > 0 and line[0].strip_edges() != "": existing_csv[line[0]] = line # The character column wasn't found in the existing file but the setting is turned on if character_column == -1 and DMSettings.get_setting(DMSettings.INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS, false): character_column = column_count column_count += 1 existing_csv["keys"].append("_character") # The notes column wasn't found in the existing file but the setting is turned on if notes_column == -1 and DMSettings.get_setting(DMSettings.INCLUDE_NOTES_IN_TRANSLATION_EXPORTS, false): notes_column = column_count column_count += 1 existing_csv["keys"].append("_notes") # Start a new file file = FileAccess.open(to_path, FileAccess.WRITE) if not FileAccess.file_exists(to_path): var headings: PackedStringArray = ["keys", default_locale] + DMSettings.get_setting(DMSettings.EXTRA_CSV_LOCALES, []) if DMSettings.get_setting(DMSettings.INCLUDE_CHARACTER_IN_TRANSLATION_EXPORTS, false): character_column = headings.size() headings.append("_character") if DMSettings.get_setting(DMSettings.INCLUDE_NOTES_IN_TRANSLATION_EXPORTS, false): notes_column = headings.size() headings.append("_notes") file.store_csv_line(headings, delimiter) column_count = headings.size() # Write our translations to file var known_keys: PackedStringArray = [] var dialogue = DMCompiler.compile_string(text, dialogue_path).lines # Make a list of stuff that needs to go into the file var lines_to_save = [] for key in dialogue.keys(): var line: Dictionary = dialogue.get(key) if not line.type in [DMConstants.TYPE_DIALOGUE, DMConstants.TYPE_RESPONSE]: continue var translation_key: String = line.get(&"translation_key", line.text) if translation_key in known_keys: continue known_keys.append(translation_key) var line_to_save: PackedStringArray = [] if existing_csv.has(translation_key): line_to_save = existing_csv.get(translation_key) line_to_save.resize(column_count) existing_csv.erase(translation_key) else: line_to_save.resize(column_count) line_to_save[0] = translation_key line_to_save[default_locale_column] = line.text if character_column > -1: line_to_save[character_column] = "(response)" if line.type == DMConstants.TYPE_RESPONSE else line.character if notes_column > -1: line_to_save[notes_column] = line.get("notes", "") lines_to_save.append(line_to_save) # Store lines in the file, starting with anything that already exists that hasn't been touched for line in existing_csv.values(): file.store_csv_line(line, delimiter) for line in lines_to_save: file.store_csv_line(line, delimiter) file.close() ## Get the delimier used for an existing CSV static func get_delimiter_for_csv(path: String) -> String: if FileAccess.file_exists(path): var import_path: String = "%s.%s" % [path, "import"] var import_file: ConfigFile = ConfigFile.new() if import_file.load(import_path) == OK: match import_file.get_value("params", "delimier", 0): 0: return "," 1: return ";" 2: return "\t" match DMSettings.get_setting(DMSettings.DEFAULT_CSV_DELIMITER, "Comma"): "Comma": return "," "Semicolon": return ";" "Tab": return "\t" return "," ## Save any character names in a file to CSV. static func export_character_names_to_csv(to_path: String, text: String, dialogue_path: String) -> void: var file: FileAccess # If the file exists, open it first and work out which keys are already in it var existing_csv = {} var delimiter: String = get_delimiter_for_csv(to_path) var commas = [] if FileAccess.file_exists(to_path): file = FileAccess.open(to_path, FileAccess.READ) var is_first_line = true var line: Array while !file.eof_reached(): line = file.get_csv_line(delimiter) if is_first_line: is_first_line = false for i in range(2, line.size()): commas.append("") # Make sure the line isn't empty before adding it if line.size() > 0 and line[0].strip_edges() != "": existing_csv[line[0]] = line # Start a new file file = FileAccess.open(to_path, FileAccess.WRITE) if not file.file_exists(to_path): file.store_csv_line(["keys", DMSettings.get_setting(DMSettings.DEFAULT_CSV_LOCALE, "en")], delimiter) # Write our translations to file var known_keys: PackedStringArray = [] var character_names: PackedStringArray = DMCompiler.compile_string(text, dialogue_path).character_names # Make a list of stuff that needs to go into the file var lines_to_save = [] for character_name in character_names: if character_name in known_keys: continue known_keys.append(character_name) if existing_csv.has(character_name): var existing_line = existing_csv.get(character_name) existing_line[1] = character_name lines_to_save.append(existing_line) existing_csv.erase(character_name) else: lines_to_save.append(PackedStringArray([character_name, character_name] + commas)) # Store lines in the file, starting with anything that already exists that hasn't been touched for line in existing_csv.values(): file.store_csv_line(line, delimiter) for line in lines_to_save: file.store_csv_line(line, delimiter) file.close() ## Replace translatable lines in some text using an existing CSV. static func import_translations_from_csv(from_path: String, text: String) -> String: if not FileAccess.file_exists(from_path): return text # Open the CSV file and build a dictionary of the known keys var delimiter: String = get_delimiter_for_csv(from_path) var keys: Dictionary = {} var file: FileAccess = FileAccess.open(from_path, FileAccess.READ) var csv_line: Array while !file.eof_reached(): csv_line = file.get_csv_line(delimiter) if csv_line.size() > 1: keys[csv_line[0]] = csv_line[1] # Now look over each line in the dialogue and replace the content for matched keys var lines: PackedStringArray = text.split("\n") var start_index: int = 0 var end_index: int = 0 for i in range(0, lines.size()): var line: String = lines[i] var translation_key: String = DMCompiler.get_static_line_id(line) if keys.has(translation_key): if DMCompiler.get_line_type(line) == DMConstants.TYPE_DIALOGUE: start_index = 0 # See if we need to skip over a character name line = line.replace("\\:", "!ESCAPED_COLON!") if ": " in line: start_index = line.find(": ") + 2 lines[i] = (line.substr(0, start_index) + keys.get(translation_key) + " [ID:" + translation_key + "]").replace("!ESCAPED_COLON!", ":") elif DMCompiler.get_line_type(line) == DMConstants.TYPE_RESPONSE: start_index = line.find("- ") + 2 # See if we need to skip over a character name line = line.replace("\\:", "!ESCAPED_COLON!") if ": " in line: start_index = line.find(": ") + 2 end_index = line.length() if " =>" in line: end_index = line.find(" =>") if " [if " in line: end_index = line.find(" [if ") lines[i] = (line.substr(0, start_index) + keys.get(translation_key) + " [ID:" + translation_key + "]" + line.substr(end_index)).replace("!ESCAPED_COLON!", ":") return "\n".join(lines)