Path: blob/master/editor/translations/template_generator.cpp
14709 views
/**************************************************************************/1/* template_generator.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "template_generator.h"3132#include "core/config/project_settings.h"33#include "editor/translations/editor_translation.h"34#include "editor/translations/editor_translation_parser.h"3536TranslationTemplateGenerator::MessageMap TranslationTemplateGenerator::parse(const Vector<String> &p_sources, bool p_add_builtin) const {37Vector<Vector<String>> raw;3839for (const String &path : p_sources) {40Vector<Vector<String>> parsed_from_file;4142const String &extension = path.get_extension();43ERR_CONTINUE_MSG(!EditorTranslationParser::get_singleton()->can_parse(extension), vformat("Cannot parse file '%s': unrecognized file extension. Skipping.", path));4445EditorTranslationParser::get_singleton()->get_parser(extension)->parse_file(path, &parsed_from_file);4647for (const Vector<String> &entry : parsed_from_file) {48ERR_CONTINUE(entry.is_empty());49if (entry[0].is_empty()) {50continue;51}5253const String &msgctxt = (entry.size() > 1) ? entry[1] : String();54const String &msgid_plural = (entry.size() > 2) ? entry[2] : String();55const String &comment = (entry.size() > 3) ? entry[3] : String();56const int source_line = (entry.size() > 4) ? entry[4].to_int() : 0;57const String &location = source_line > 0 ? vformat("%s:%d", path, source_line) : path;5859raw.push_back({ entry[0], msgctxt, msgid_plural, comment, location });60}61}6263if (p_add_builtin) {64for (const Vector<String> &extractable_msgids : get_extractable_message_list()) {65raw.push_back({ extractable_msgids[0], extractable_msgids[1], extractable_msgids[2], String(), String() });66}67}6869if (GLOBAL_GET("application/config/name_localized").operator Dictionary().is_empty()) {70const String &project_name = GLOBAL_GET("application/config/name");71if (!project_name.is_empty()) {72raw.push_back({ project_name, String(), String(), String(), String() });73}74}7576MessageMap result;77for (const Vector<String> &entry : raw) {78const String &msgid = entry[0];79const String &msgctxt = entry[1];80const String &plural = entry[2];81const String &comment = entry[3];82const String &location = entry[4];8384const Translation::MessageKey key = { msgctxt, msgid };85MessageData &mdata = result[key];86if (!mdata.plural.is_empty() && !plural.is_empty() && mdata.plural != plural) {87WARN_PRINT(vformat(R"(Skipping different plural definitions for msgid "%s" msgctxt "%s": "%s" and "%s")", msgid, msgctxt, mdata.plural, plural));88continue;89}90mdata.plural = plural;91if (!location.is_empty()) {92mdata.locations.insert(location);93}94if (!comment.is_empty()) {95mdata.comments.insert(comment);96}97}98return result;99}100101void TranslationTemplateGenerator::generate(const String &p_file) {102const Vector<String> files = GLOBAL_GET("internationalization/locale/translations_pot_files");103const bool add_builtin = GLOBAL_GET("internationalization/locale/translation_add_builtin_strings_to_pot");104105const MessageMap &map = parse(files, add_builtin);106if (map.is_empty()) {107WARN_PRINT_ED(TTR("No translatable strings found."));108return;109}110111Error err;112Ref<FileAccess> file = FileAccess::open(p_file, FileAccess::WRITE, &err);113ERR_FAIL_COND_MSG(err != OK, "Failed to open " + p_file);114115const String ext = p_file.get_extension().to_lower();116if (ext == "pot") {117_write_to_pot(file, map);118} else if (ext == "csv") {119_write_to_csv(file, map);120} else {121ERR_FAIL_MSG("Unrecognized translation template file extension: " + ext);122}123}124125static void _write_pot_field(Ref<FileAccess> p_file, const String &p_name, const String &p_value) {126p_file->store_string(p_name + " ");127128if (p_value.is_empty()) {129p_file->store_line("\"\"");130return;131}132133const Vector<String> lines = p_value.split("\n");134DEV_ASSERT(lines.size() > 0);135136const String &last_line = lines[lines.size() - 1];137const int pot_line_count = last_line.is_empty() ? lines.size() - 1 : lines.size();138139if (pot_line_count > 1) {140p_file->store_line("\"\"");141}142143for (int i = 0; i < lines.size() - 1; i++) {144p_file->store_line((lines[i] + "\n").json_escape().quote());145}146if (!last_line.is_empty()) {147p_file->store_line(last_line.json_escape().quote());148}149}150151void TranslationTemplateGenerator::_write_to_pot(Ref<FileAccess> p_file, const MessageMap &p_map) const {152const String project_name = GLOBAL_GET("application/config/name").operator String().replace("\n", "\\n");153const Vector<String> files = GLOBAL_GET("internationalization/locale/translations_pot_files");154String extracted_files;155for (const String &file : files) {156extracted_files += "# " + file.replace("\n", "\\n") + "\n";157}158const String header =159"# LANGUAGE translation for " + project_name + " for the following files:\n" +160extracted_files +161"#\n"162"# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n"163"#\n"164"#, fuzzy\n"165"msgid \"\"\n"166"msgstr \"\"\n"167"\"Project-Id-Version: " +168project_name +169"\\n\"\n"170"\"MIME-Version: 1.0\\n\"\n"171"\"Content-Type: text/plain; charset=UTF-8\\n\"\n"172"\"Content-Transfer-Encoding: 8-bit\\n\"\n";173p_file->store_string(header);174175for (const KeyValue<Translation::MessageKey, MessageData> &E : p_map) {176// Put the blank line at the start, to avoid a double at the end when closing the file.177p_file->store_line("");178179// Write comments.180bool is_first_comment = true;181for (const String &comment : E.value.comments) {182if (is_first_comment) {183p_file->store_line("#. TRANSLATORS: " + comment.replace("\n", "\n#. "));184} else {185p_file->store_line("#. " + comment.replace("\n", "\n#. "));186}187is_first_comment = false;188}189190// Write file locations.191for (const String &location : E.value.locations) {192p_file->store_line("#: " + location.trim_prefix("res://").replace("\n", "\\n"));193}194195// Write context.196const String msgctxt = E.key.msgctxt;197if (!msgctxt.is_empty()) {198p_file->store_line("msgctxt " + msgctxt.json_escape().quote());199}200201// Write msgid.202_write_pot_field(p_file, "msgid", E.key.msgid);203204// Write msgid_plural.205if (E.value.plural.is_empty()) {206p_file->store_line("msgstr \"\"");207} else {208_write_pot_field(p_file, "msgid_plural", E.value.plural);209p_file->store_line("msgstr[0] \"\"");210p_file->store_line("msgstr[1] \"\"");211}212}213}214215static String _join_strings(const HashSet<String> &p_strings) {216String result;217bool is_first = true;218for (const String &s : p_strings) {219if (!is_first) {220result += '\n';221}222result += s;223is_first = false;224}225return result;226}227228void TranslationTemplateGenerator::_write_to_csv(Ref<FileAccess> p_file, const MessageMap &p_map) const {229// Avoid adding unnecessary columns.230bool context_used = false;231bool plural_used = false;232bool comments_used = false;233bool locations_used = false;234{235for (const KeyValue<Translation::MessageKey, MessageData> &E : p_map) {236if (!context_used && !E.key.msgctxt.is_empty()) {237context_used = true;238}239if (!plural_used && !E.value.plural.is_empty()) {240plural_used = true;241}242if (!comments_used && !E.value.comments.is_empty()) {243comments_used = true;244}245if (!locations_used && !E.value.locations.is_empty()) {246locations_used = true;247}248}249}250251Vector<String> header = { "key" };252if (context_used) {253header.push_back("?context");254}255if (plural_used) {256header.push_back("?plural");257}258if (comments_used) {259header.push_back("_comments");260}261if (locations_used) {262header.push_back("_locations");263}264p_file->store_csv_line(header);265266for (const KeyValue<Translation::MessageKey, MessageData> &E : p_map) {267Vector<String> line = { E.key.msgid };268if (context_used) {269line.push_back(E.key.msgctxt);270}271if (plural_used) {272line.push_back(E.value.plural);273}274if (comments_used) {275line.push_back(_join_strings(E.value.comments));276}277if (locations_used) {278line.push_back(_join_strings(E.value.locations));279}280p_file->store_csv_line(line);281}282}283284TranslationTemplateGenerator *TranslationTemplateGenerator::get_singleton() {285if (!singleton) {286singleton = memnew(TranslationTemplateGenerator);287}288return singleton;289}290291TranslationTemplateGenerator::~TranslationTemplateGenerator() {292memdelete(singleton);293singleton = nullptr;294}295296297