Skip to content

Commit

Permalink
Implement gettext translation file generation
Browse files Browse the repository at this point in the history
  • Loading branch information
jrb0001 committed Aug 2, 2024
1 parent 7f1b837 commit 58729cc
Show file tree
Hide file tree
Showing 10 changed files with 724 additions and 618 deletions.
124 changes: 124 additions & 0 deletions addons/dialogic/Editor/Settings/TranslationFiles/csv.gd
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 addons/dialogic/Editor/Settings/TranslationFiles/gettext.gd
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] = []
Loading

0 comments on commit 58729cc

Please sign in to comment.