Files
Dawn-Godot/addons/dialogue_manager/utilities/translations.gd
T
2026-06-11 19:59:31 -05:00

289 lines
10 KiB
GDScript

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