Files
godot/editor/translations/template_generator.cpp
2025-11-21 21:11:41 +01:00

296 lines
10 KiB
C++

/**************************************************************************/
/* template_generator.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "template_generator.h"
#include "core/config/project_settings.h"
#include "editor/translations/editor_translation.h"
#include "editor/translations/editor_translation_parser.h"
TranslationTemplateGenerator::MessageMap TranslationTemplateGenerator::parse(const Vector<String> &p_sources, bool p_add_builtin) const {
Vector<Vector<String>> raw;
for (const String &path : p_sources) {
Vector<Vector<String>> parsed_from_file;
const String &extension = path.get_extension();
ERR_CONTINUE_MSG(!EditorTranslationParser::get_singleton()->can_parse(extension), vformat("Cannot parse file '%s': unrecognized file extension. Skipping.", path));
EditorTranslationParser::get_singleton()->get_parser(extension)->parse_file(path, &parsed_from_file);
for (const Vector<String> &entry : parsed_from_file) {
ERR_CONTINUE(entry.is_empty());
if (entry[0].is_empty()) {
continue;
}
const String &msgctxt = (entry.size() > 1) ? entry[1] : String();
const String &msgid_plural = (entry.size() > 2) ? entry[2] : String();
const String &comment = (entry.size() > 3) ? entry[3] : String();
const int source_line = (entry.size() > 4) ? entry[4].to_int() : 0;
const String &location = source_line > 0 ? vformat("%s:%d", path, source_line) : path;
raw.push_back({ entry[0], msgctxt, msgid_plural, comment, location });
}
}
if (p_add_builtin) {
for (const Vector<String> &extractable_msgids : get_extractable_message_list()) {
raw.push_back({ extractable_msgids[0], extractable_msgids[1], extractable_msgids[2], String(), String() });
}
}
if (GLOBAL_GET("application/config/name_localized").operator Dictionary().is_empty()) {
const String &project_name = GLOBAL_GET("application/config/name");
if (!project_name.is_empty()) {
raw.push_back({ project_name, String(), String(), String(), String() });
}
}
MessageMap result;
for (const Vector<String> &entry : raw) {
const String &msgid = entry[0];
const String &msgctxt = entry[1];
const String &plural = entry[2];
const String &comment = entry[3];
const String &location = entry[4];
const Translation::MessageKey key = { msgctxt, msgid };
MessageData &mdata = result[key];
if (!mdata.plural.is_empty() && !plural.is_empty() && mdata.plural != plural) {
WARN_PRINT(vformat(R"(Skipping different plural definitions for msgid "%s" msgctxt "%s": "%s" and "%s")", msgid, msgctxt, mdata.plural, plural));
continue;
}
mdata.plural = plural;
if (!location.is_empty()) {
mdata.locations.insert(location);
}
if (!comment.is_empty()) {
mdata.comments.insert(comment);
}
}
return result;
}
void TranslationTemplateGenerator::generate(const String &p_file) {
const Vector<String> files = GLOBAL_GET("internationalization/locale/translations_pot_files");
const bool add_builtin = GLOBAL_GET("internationalization/locale/translation_add_builtin_strings_to_pot");
const MessageMap &map = parse(files, add_builtin);
if (map.is_empty()) {
WARN_PRINT_ED(TTR("No translatable strings found."));
return;
}
Error err;
Ref<FileAccess> file = FileAccess::open(p_file, FileAccess::WRITE, &err);
ERR_FAIL_COND_MSG(err != OK, "Failed to open " + p_file);
const String ext = p_file.get_extension().to_lower();
if (ext == "pot") {
_write_to_pot(file, map);
} else if (ext == "csv") {
_write_to_csv(file, map);
} else {
ERR_FAIL_MSG("Unrecognized translation template file extension: " + ext);
}
}
static void _write_pot_field(Ref<FileAccess> p_file, const String &p_name, const String &p_value) {
p_file->store_string(p_name + " ");
if (p_value.is_empty()) {
p_file->store_line("\"\"");
return;
}
const Vector<String> lines = p_value.split("\n");
DEV_ASSERT(lines.size() > 0);
const String &last_line = lines[lines.size() - 1];
const int pot_line_count = last_line.is_empty() ? lines.size() - 1 : lines.size();
if (pot_line_count > 1) {
p_file->store_line("\"\"");
}
for (int i = 0; i < lines.size() - 1; i++) {
p_file->store_line((lines[i] + "\n").json_escape().quote());
}
if (!last_line.is_empty()) {
p_file->store_line(last_line.json_escape().quote());
}
}
void TranslationTemplateGenerator::_write_to_pot(Ref<FileAccess> p_file, const MessageMap &p_map) const {
const String project_name = GLOBAL_GET("application/config/name").operator String().replace("\n", "\\n");
const Vector<String> files = GLOBAL_GET("internationalization/locale/translations_pot_files");
String extracted_files;
for (const String &file : files) {
extracted_files += "# " + file.replace("\n", "\\n") + "\n";
}
const String header =
"# LANGUAGE translation for " + project_name + " for the following files:\n" +
extracted_files +
"#\n"
"# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n"
"#\n"
"#, fuzzy\n"
"msgid \"\"\n"
"msgstr \"\"\n"
"\"Project-Id-Version: " +
project_name +
"\\n\"\n"
"\"MIME-Version: 1.0\\n\"\n"
"\"Content-Type: text/plain; charset=UTF-8\\n\"\n"
"\"Content-Transfer-Encoding: 8-bit\\n\"\n";
p_file->store_string(header);
for (const KeyValue<Translation::MessageKey, MessageData> &E : p_map) {
// Put the blank line at the start, to avoid a double at the end when closing the file.
p_file->store_line("");
// Write comments.
bool is_first_comment = true;
for (const String &comment : E.value.comments) {
if (is_first_comment) {
p_file->store_line("#. TRANSLATORS: " + comment.replace("\n", "\n#. "));
} else {
p_file->store_line("#. " + comment.replace("\n", "\n#. "));
}
is_first_comment = false;
}
// Write file locations.
for (const String &location : E.value.locations) {
p_file->store_line("#: " + location.trim_prefix("res://").replace("\n", "\\n"));
}
// Write context.
const String msgctxt = E.key.msgctxt;
if (!msgctxt.is_empty()) {
p_file->store_line("msgctxt " + msgctxt.json_escape().quote());
}
// Write msgid.
_write_pot_field(p_file, "msgid", E.key.msgid);
// Write msgid_plural.
if (E.value.plural.is_empty()) {
p_file->store_line("msgstr \"\"");
} else {
_write_pot_field(p_file, "msgid_plural", E.value.plural);
p_file->store_line("msgstr[0] \"\"");
p_file->store_line("msgstr[1] \"\"");
}
}
}
static String _join_strings(const HashSet<String> &p_strings) {
String result;
bool is_first = true;
for (const String &s : p_strings) {
if (!is_first) {
result += '\n';
}
result += s;
is_first = false;
}
return result;
}
void TranslationTemplateGenerator::_write_to_csv(Ref<FileAccess> p_file, const MessageMap &p_map) const {
// Avoid adding unnecessary columns.
bool context_used = false;
bool plural_used = false;
bool comments_used = false;
bool locations_used = false;
{
for (const KeyValue<Translation::MessageKey, MessageData> &E : p_map) {
if (!context_used && !E.key.msgctxt.is_empty()) {
context_used = true;
}
if (!plural_used && !E.value.plural.is_empty()) {
plural_used = true;
}
if (!comments_used && !E.value.comments.is_empty()) {
comments_used = true;
}
if (!locations_used && !E.value.locations.is_empty()) {
locations_used = true;
}
}
}
Vector<String> header = { "key" };
if (context_used) {
header.push_back("?context");
}
if (plural_used) {
header.push_back("?plural");
}
if (comments_used) {
header.push_back("_comments");
}
if (locations_used) {
header.push_back("_locations");
}
p_file->store_csv_line(header);
for (const KeyValue<Translation::MessageKey, MessageData> &E : p_map) {
Vector<String> line = { E.key.msgid };
if (context_used) {
line.push_back(E.key.msgctxt);
}
if (plural_used) {
line.push_back(E.value.plural);
}
if (comments_used) {
line.push_back(_join_strings(E.value.comments));
}
if (locations_used) {
line.push_back(_join_strings(E.value.locations));
}
p_file->store_csv_line(line);
}
}
TranslationTemplateGenerator *TranslationTemplateGenerator::get_singleton() {
if (!singleton) {
singleton = memnew(TranslationTemplateGenerator);
}
return singleton;
}
TranslationTemplateGenerator::~TranslationTemplateGenerator() {
memdelete(singleton);
singleton = nullptr;
}