-
-
Notifications
You must be signed in to change notification settings - Fork 244
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement gettext translation file generation
- Loading branch information
Showing
10 changed files
with
724 additions
and
618 deletions.
There are no files selected for viewing
124 changes: 124 additions & 0 deletions
124
addons/dialogic/Editor/Settings/TranslationFiles/csv.gd
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
class_name DialogicTranslationCsvFile | ||
extends DialogicTranslationFile | ||
## Generates translation files in CSV format. | ||
|
||
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 | ||
|
||
## The underlying file used to read and write the CSV file. | ||
var file: FileAccess | ||
|
||
## 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 | ||
|
||
|
||
## 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: | ||
super._init(file_path, original_locale) | ||
|
||
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 is_new_file: | ||
# 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 | ||
|
||
|
||
func _append(key: String, value: String, _path: String, _line_number: int = -1) -> void: | ||
var array_line := PackedStringArray([key, value]) | ||
lines.append(array_line) | ||
|
||
|
||
## Appends an empty line to the [member lines] array. | ||
func _append_separator() -> void: | ||
if add_separator: | ||
var empty_line := PackedStringArray(["", ""]) | ||
lines.append(empty_line) | ||
|
||
|
||
## 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_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() |
148 changes: 148 additions & 0 deletions
148
addons/dialogic/Editor/Settings/TranslationFiles/gettext.gd
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
class_name DialogicTranslationGettextFile | ||
extends DialogicTranslationFile | ||
## Generates translation files in gettext format. | ||
|
||
var translations: Array[PotEntry] = [] | ||
|
||
## Configured original locale. | ||
var original_locale: String | ||
|
||
## Locations of the source files included in this translation. | ||
var locations: Array[String] = [] | ||
|
||
|
||
## There is no need to load the old file(s) here, because every locale has its own file | ||
## and this class doens't touch them. | ||
func _init(file_path: String, original_locale: String) -> void: | ||
super._init(file_path, original_locale) | ||
self.original_locale = original_locale | ||
|
||
|
||
func _append(key: String, value: String, path: String, line_number: int = -1) -> void: | ||
var entry = PotEntry.new() | ||
entry.key = key | ||
entry.translation = value | ||
entry.locations.append(PotReference.new(path, line_number)) | ||
translations.append(entry) | ||
|
||
|
||
## gettext doesn't support separators so this is a no-op. | ||
func _append_separator() -> void: | ||
pass | ||
|
||
|
||
## Overwrites the .pot file and the .po file of the original locale with the current [member translations] array. | ||
func update_file_on_disk() -> void: | ||
# Overwrite the POT file. | ||
var file = FileAccess.open(used_file_path, FileAccess.WRITE) | ||
_write_header(file) | ||
for entry in translations: | ||
_write_entry(file, entry, "") | ||
file.close() | ||
|
||
# Overwrite the original_locale PO file. | ||
file = FileAccess.open(used_file_path.trim_suffix(".pot") + "." + original_locale + ".po", FileAccess.WRITE) | ||
_write_header(file, original_locale) | ||
for entry in translations: | ||
_write_entry(file, entry) | ||
file.close() | ||
|
||
|
||
# This is based on POTGenerator::_write_to_pot() which unfortunately isn't exposed to gdscript. | ||
func _write_header(file: FileAccess, locale: String = "") -> void: | ||
var project_name = ProjectSettings.get("application/config/name"); | ||
var language_header = locale if !locale.is_empty() else "LANGUAGE" | ||
file.store_line("# " + language_header + " translation for " + project_name + " for the following files:") | ||
|
||
locations.sort() | ||
for location in locations: | ||
file.store_line("# " + location) | ||
|
||
file.store_line("") | ||
file.store_line("#, fuzzy"); | ||
file.store_line("msgid \"\"") | ||
file.store_line("msgstr \"\"") | ||
file.store_line("\"Project-Id-Version: " + project_name + "\\n\"") | ||
if !locale.is_empty(): | ||
file.store_line("\"Language: " + locale + "\\n\"") | ||
file.store_line("\"MIME-Version: 1.0\\n\"") | ||
file.store_line("\"Content-Type: text/plain; charset=UTF-8\\n\"") | ||
file.store_line("\"Content-Transfer-Encoding: 8-bit\\n\"") | ||
|
||
|
||
func _write_entry(file: FileAccess, entry: PotEntry, value: String = entry.translation) -> void: | ||
file.store_line("") | ||
|
||
entry.locations.sort_custom(func (a: String, b: String): return b > a) | ||
for location in entry.locations: | ||
file.store_line("#: " + location.as_str()) | ||
|
||
_write_line(file, "msgid", entry.key) | ||
_write_line(file, "msgstr", value) | ||
|
||
|
||
# This is based on POTGenerator::_write_msgid() which unfortunately isn't exposed to gdscript. | ||
func _write_line(file: FileAccess, type: String, value: String) -> void: | ||
file.store_string(type + " ") | ||
if value.is_empty(): | ||
file.store_line("\"\"") | ||
return | ||
|
||
var lines = value.split("\n") | ||
var last_line = lines[lines.size() - 1] | ||
var pot_line_count = lines.size() | ||
if last_line.is_empty(): | ||
pot_line_count -= 1 | ||
|
||
if pot_line_count > 1: | ||
file.store_line("\"\"") | ||
|
||
for i in range(0, lines.size() - 1): | ||
file.store_line("\"" + (lines[i] + "\n").json_escape() + "\"") | ||
|
||
if !last_line.is_empty(): | ||
file.store_line("\"" + last_line.json_escape() + "\"") | ||
|
||
|
||
func collect_lines_from_character(character: DialogicCharacter) -> void: | ||
super.collect_lines_from_character(character) | ||
locations.append(character.resource_path) | ||
|
||
|
||
func collect_lines_from_glossary(glossary: DialogicGlossary) -> void: | ||
super.collect_lines_from_glossary(glossary) | ||
locations.append(glossary.resource_path) | ||
|
||
|
||
func collect_lines_from_timeline(timeline: DialogicTimeline) -> void: | ||
super.collect_lines_from_timeline(timeline) | ||
locations.append(timeline.resource_path) | ||
|
||
|
||
class PotReference: | ||
var path: String | ||
var line_number: int | ||
|
||
|
||
func _init(path: String, line_number: int) -> void: | ||
self.path = path | ||
self.line_number = line_number | ||
|
||
|
||
func as_str() -> String: | ||
var str = "" | ||
if path.contains(" "): | ||
str += "\u2068" + path.trim_prefix("res://").replace("\n", "\\n") + "\u2069" | ||
else: | ||
str += path.trim_prefix("res://").replace("\n", "\\n") | ||
|
||
if line_number >= 0: | ||
str += ":" + str(line_number) | ||
|
||
return str | ||
|
||
|
||
class PotEntry: | ||
var key: String | ||
var translation: String | ||
var locations: Array[PotReference] = [] |
Oops, something went wrong.