1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-18 14:21:41 +00:00

Add CSV translation template generation

This commit is contained in:
Haoyu Qiu
2025-10-29 17:44:13 +08:00
parent 07f4c06601
commit ea9a2c3b2c
9 changed files with 404 additions and 374 deletions

View File

@@ -40,8 +40,7 @@ class Translation : public Resource {
OBJ_SAVE_TYPE(Translation); OBJ_SAVE_TYPE(Translation);
RES_BASE_EXTENSION("translation"); RES_BASE_EXTENSION("translation");
String locale = "en"; public:
struct MessageKey { struct MessageKey {
StringName msgctxt; StringName msgctxt;
StringName msgid; StringName msgid;
@@ -56,6 +55,9 @@ class Translation : public Resource {
} }
}; };
private:
String locale = "en";
HashMap<MessageKey, Vector<StringName>, MessageKey> translation_map; HashMap<MessageKey, Vector<StringName>, MessageKey> translation_map;
mutable PluralRules *plural_rules_cache = nullptr; mutable PluralRules *plural_rules_cache = nullptr;

View File

@@ -6,8 +6,8 @@
<description> <description>
[EditorTranslationParserPlugin] is invoked when a file is being parsed to extract strings that require translation. To define the parsing and string extraction logic, override the [method _parse_file] method in script. [EditorTranslationParserPlugin] is invoked when a file is being parsed to extract strings that require translation. To define the parsing and string extraction logic, override the [method _parse_file] method in script.
The return value should be an [Array] of [PackedStringArray]s, one for each extracted translatable string. Each entry should contain [code][msgid, msgctxt, msgid_plural, comment, source_line][/code], where all except [code]msgid[/code] are optional. Empty strings will be ignored. The return value should be an [Array] of [PackedStringArray]s, one for each extracted translatable string. Each entry should contain [code][msgid, msgctxt, msgid_plural, comment, source_line][/code], where all except [code]msgid[/code] are optional. Empty strings will be ignored.
The extracted strings will be written into a POT file selected by user under "POT Generation" in "Localization" tab in "Project Settings" menu. The extracted strings will be written into a translation template file selected by user under "Template Generation" in "Localization" tab in "Project Settings" menu.
Below shows an example of a custom parser that extracts strings from a CSV file to write into a POT. Below shows an example of a custom parser that extracts strings from a CSV file to write into a template.
[codeblocks] [codeblocks]
[gdscript] [gdscript]
@tool @tool

View File

@@ -1037,7 +1037,7 @@
</methods> </methods>
<members> <members>
<member name="auto_translate_mode" type="int" setter="set_auto_translate_mode" getter="get_auto_translate_mode" enum="Node.AutoTranslateMode" default="0"> <member name="auto_translate_mode" type="int" setter="set_auto_translate_mode" getter="get_auto_translate_mode" enum="Node.AutoTranslateMode" default="0">
Defines if any text should automatically change to its translated version depending on the current locale (for nodes such as [Label], [RichTextLabel], [Window], etc.). Also decides if the node's strings should be parsed for POT generation. Defines if any text should automatically change to its translated version depending on the current locale (for nodes such as [Label], [RichTextLabel], [Window], etc.). Also decides if the node's strings should be parsed for translation template generation.
[b]Note:[/b] For the root node, auto translate mode can also be set via [member ProjectSettings.internationalization/rendering/root_node_auto_translate]. [b]Note:[/b] For the root node, auto translate mode can also be set via [member ProjectSettings.internationalization/rendering/root_node_auto_translate].
</member> </member>
<member name="editor_description" type="String" setter="set_editor_description" getter="get_editor_description" default="&quot;&quot;"> <member name="editor_description" type="String" setter="set_editor_description" getter="get_editor_description" default="&quot;&quot;">
@@ -1397,7 +1397,7 @@
</constant> </constant>
<constant name="AUTO_TRANSLATE_MODE_DISABLED" value="2" enum="AutoTranslateMode"> <constant name="AUTO_TRANSLATE_MODE_DISABLED" value="2" enum="AutoTranslateMode">
Never automatically translate. This is the inverse of [constant AUTO_TRANSLATE_MODE_ALWAYS]. Never automatically translate. This is the inverse of [constant AUTO_TRANSLATE_MODE_ALWAYS].
String parsing for POT generation will be skipped for this node and children that are set to [constant AUTO_TRANSLATE_MODE_INHERIT]. String parsing for translation template generation will be skipped for this node and children that are set to [constant AUTO_TRANSLATE_MODE_INHERIT].
</constant> </constant>
</constants> </constants>
</class> </class>

View File

@@ -121,7 +121,10 @@ Error ResourceImporterCSVTranslation::import(ResourceUID::ID p_source_id, const
column_to_translation[i] = translation; column_to_translation[i] = translation;
} }
ERR_FAIL_COND_V_MSG(column_to_translation.is_empty(), ERR_PARSE_ERROR, "Error importing CSV translation: The CSV file must have at least one column for key and one column for translation."); if (column_to_translation.is_empty()) {
WARN_PRINT(vformat("CSV file '%s' does not contain any translation.", p_source_file));
return OK;
}
} }
// Parse content rows. // Parse content rows.

View File

@@ -37,7 +37,7 @@
#include "editor/gui/editor_file_dialog.h" #include "editor/gui/editor_file_dialog.h"
#include "editor/settings/editor_settings.h" #include "editor/settings/editor_settings.h"
#include "editor/translations/editor_translation_parser.h" #include "editor/translations/editor_translation_parser.h"
#include "editor/translations/pot_generator.h" #include "editor/translations/template_generator.h"
#include "scene/gui/control.h" #include "scene/gui/control.h"
#include "scene/gui/tab_container.h" #include "scene/gui/tab_container.h"
@@ -45,8 +45,8 @@ void LocalizationEditor::_notification(int p_what) {
switch (p_what) { switch (p_what) {
case NOTIFICATION_ENTER_TREE: { case NOTIFICATION_ENTER_TREE: {
translation_list->connect("button_clicked", callable_mp(this, &LocalizationEditor::_translation_delete)); translation_list->connect("button_clicked", callable_mp(this, &LocalizationEditor::_translation_delete));
translation_pot_list->connect("button_clicked", callable_mp(this, &LocalizationEditor::_pot_delete)); template_source_list->connect("button_clicked", callable_mp(this, &LocalizationEditor::_template_source_delete));
translation_pot_add_builtin->set_pressed(GLOBAL_GET("internationalization/locale/translation_add_builtin_strings_to_pot")); template_add_builtin->set_pressed(GLOBAL_GET("internationalization/locale/translation_add_builtin_strings_to_pot"));
List<String> tfn; List<String> tfn;
ResourceLoader::get_recognized_extensions_for_type("Translation", &tfn); ResourceLoader::get_recognized_extensions_for_type("Translation", &tfn);
@@ -62,8 +62,9 @@ void LocalizationEditor::_notification(int p_what) {
translation_res_option_file_open_dialog->add_filter("*." + E); translation_res_option_file_open_dialog->add_filter("*." + E);
} }
_update_pot_file_extensions(); _update_template_source_file_extensions();
pot_generate_dialog->add_filter("*.pot"); template_generate_dialog->add_filter("*.pot");
template_generate_dialog->add_filter("*.csv");
} break; } break;
case NOTIFICATION_DRAG_END: { case NOTIFICATION_DRAG_END: {
@@ -342,12 +343,12 @@ void LocalizationEditor::_translation_res_option_delete(Object *p_item, int p_co
undo_redo->commit_action(); undo_redo->commit_action();
} }
void LocalizationEditor::_pot_add(const PackedStringArray &p_paths) { void LocalizationEditor::_template_source_add(const PackedStringArray &p_paths) {
PackedStringArray pot_translations = GLOBAL_GET("internationalization/locale/translations_pot_files"); PackedStringArray sources = GLOBAL_GET("internationalization/locale/translations_pot_files");
int count = 0; int count = 0;
for (const String &path : p_paths) { for (const String &path : p_paths) {
if (!pot_translations.has(path)) { if (!sources.has(path)) {
pot_translations.push_back(path); sources.push_back(path);
count += 1; count += 1;
} }
} }
@@ -356,8 +357,8 @@ void LocalizationEditor::_pot_add(const PackedStringArray &p_paths) {
} }
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(vformat(TTRN("Add %d file for POT generation", "Add %d files for POT generation", count), count)); undo_redo->create_action(vformat(TTRN("Add %d file for template generation", "Add %d files for template generation", count), count));
undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", pot_translations); undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", sources);
undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", GLOBAL_GET("internationalization/locale/translations_pot_files")); undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", GLOBAL_GET("internationalization/locale/translations_pot_files"));
undo_redo->add_do_method(this, "update_translations"); undo_redo->add_do_method(this, "update_translations");
undo_redo->add_undo_method(this, "update_translations"); undo_redo->add_undo_method(this, "update_translations");
@@ -366,7 +367,7 @@ void LocalizationEditor::_pot_add(const PackedStringArray &p_paths) {
undo_redo->commit_action(); undo_redo->commit_action();
} }
void LocalizationEditor::_pot_delete(Object *p_item, int p_column, int p_button, MouseButton p_mouse_button) { void LocalizationEditor::_template_source_delete(Object *p_item, int p_column, int p_button, MouseButton p_mouse_button) {
if (p_mouse_button != MouseButton::LEFT) { if (p_mouse_button != MouseButton::LEFT) {
return; return;
} }
@@ -376,15 +377,15 @@ void LocalizationEditor::_pot_delete(Object *p_item, int p_column, int p_button,
int idx = ti->get_metadata(0); int idx = ti->get_metadata(0);
PackedStringArray pot_translations = GLOBAL_GET("internationalization/locale/translations_pot_files"); PackedStringArray sources = GLOBAL_GET("internationalization/locale/translations_pot_files");
ERR_FAIL_INDEX(idx, pot_translations.size()); ERR_FAIL_INDEX(idx, sources.size());
pot_translations.remove_at(idx); sources.remove_at(idx);
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Remove file from POT generation")); undo_redo->create_action(TTR("Remove file from template generation"));
undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", pot_translations); undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", sources);
undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", GLOBAL_GET("internationalization/locale/translations_pot_files")); undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", GLOBAL_GET("internationalization/locale/translations_pot_files"));
undo_redo->add_do_method(this, "update_translations"); undo_redo->add_do_method(this, "update_translations");
undo_redo->add_undo_method(this, "update_translations"); undo_redo->add_undo_method(this, "update_translations");
@@ -393,30 +394,35 @@ void LocalizationEditor::_pot_delete(Object *p_item, int p_column, int p_button,
undo_redo->commit_action(); undo_redo->commit_action();
} }
void LocalizationEditor::_pot_file_open() { void LocalizationEditor::_template_source_file_open() {
pot_file_open_dialog->popup_file_dialog(); template_source_open_dialog->popup_file_dialog();
} }
void LocalizationEditor::_pot_generate_open() { void LocalizationEditor::_template_generate_open() {
pot_generate_dialog->popup_file_dialog(); template_generate_dialog->popup_file_dialog();
} }
void LocalizationEditor::_pot_add_builtin_toggled() { void LocalizationEditor::_template_add_builtin_toggled() {
ProjectSettings::get_singleton()->set_setting("internationalization/locale/translation_add_builtin_strings_to_pot", translation_pot_add_builtin->is_pressed()); ProjectSettings::get_singleton()->set_setting("internationalization/locale/translation_add_builtin_strings_to_pot", template_add_builtin->is_pressed());
ProjectSettings::get_singleton()->save(); ProjectSettings::get_singleton()->save();
const PackedStringArray sources = GLOBAL_GET("internationalization/locale/translations_pot_files");
if (sources.is_empty()) {
template_generate_button->set_disabled(!template_add_builtin->is_pressed());
}
} }
void LocalizationEditor::_pot_generate(const String &p_file) { void LocalizationEditor::_template_generate(const String &p_file) {
EditorSettings::get_singleton()->set_project_metadata("pot_generator", "last_pot_path", p_file); EditorSettings::get_singleton()->set_project_metadata("pot_generator", "last_pot_path", p_file);
POTGenerator::get_singleton()->generate_pot(p_file); TranslationTemplateGenerator::get_singleton()->generate(p_file);
} }
void LocalizationEditor::_update_pot_file_extensions() { void LocalizationEditor::_update_template_source_file_extensions() {
pot_file_open_dialog->clear_filters(); template_source_open_dialog->clear_filters();
List<String> translation_parse_file_extensions; List<String> translation_parse_file_extensions;
EditorTranslationParser::get_singleton()->get_recognized_extensions(&translation_parse_file_extensions); EditorTranslationParser::get_singleton()->get_recognized_extensions(&translation_parse_file_extensions);
for (const String &E : translation_parse_file_extensions) { for (const String &E : translation_parse_file_extensions) {
pot_file_open_dialog->add_filter("*." + E); template_source_open_dialog->add_filter("*." + E);
} }
} }
@@ -426,15 +432,15 @@ void LocalizationEditor::connect_filesystem_dock_signals(FileSystemDock *p_fs_do
} }
void LocalizationEditor::_filesystem_files_moved(const String &p_old_file, const String &p_new_file) { void LocalizationEditor::_filesystem_files_moved(const String &p_old_file, const String &p_new_file) {
// Update POT files if the moved file is a part of them. // Update source files if the moved file is a part of them.
PackedStringArray pot_translations = GLOBAL_GET("internationalization/locale/translations_pot_files"); PackedStringArray sources = GLOBAL_GET("internationalization/locale/translations_pot_files");
if (pot_translations.has(p_old_file)) { if (sources.has(p_old_file)) {
pot_translations.erase(p_old_file); sources.erase(p_old_file);
ProjectSettings::get_singleton()->set_setting("internationalization/locale/translations_pot_files", pot_translations); ProjectSettings::get_singleton()->set_setting("internationalization/locale/translations_pot_files", sources);
PackedStringArray new_file; PackedStringArray new_file;
new_file.push_back(p_new_file); new_file.push_back(p_new_file);
_pot_add(new_file); _template_source_add(new_file);
} }
// Update remaps if the moved file is a part of them. // Update remaps if the moved file is a part of them.
@@ -488,11 +494,11 @@ void LocalizationEditor::_filesystem_files_moved(const String &p_old_file, const
} }
void LocalizationEditor::_filesystem_file_removed(const String &p_file) { void LocalizationEditor::_filesystem_file_removed(const String &p_file) {
// Check if the POT files are affected. // Check if the source files are affected.
PackedStringArray pot_translations = GLOBAL_GET("internationalization/locale/translations_pot_files"); PackedStringArray sources = GLOBAL_GET("internationalization/locale/translations_pot_files");
if (pot_translations.has(p_file)) { if (sources.has(p_file)) {
pot_translations.erase(p_file); sources.erase(p_file);
ProjectSettings::get_singleton()->set_setting("internationalization/locale/translations_pot_files", pot_translations); ProjectSettings::get_singleton()->set_setting("internationalization/locale/translations_pot_files", sources);
} }
// Check if the remaps are affected. // Check if the remaps are affected.
@@ -701,24 +707,24 @@ void LocalizationEditor::update_translations() {
} }
} }
// Update translation POT files. // Update translation source files.
translation_pot_list->clear(); template_source_list->clear();
root = translation_pot_list->create_item(nullptr); root = template_source_list->create_item(nullptr);
translation_pot_list->set_hide_root(true); template_source_list->set_hide_root(true);
PackedStringArray pot_translations = GLOBAL_GET("internationalization/locale/translations_pot_files"); PackedStringArray sources = GLOBAL_GET("internationalization/locale/translations_pot_files");
for (int i = 0; i < pot_translations.size(); i++) { for (int i = 0; i < sources.size(); i++) {
TreeItem *t = translation_pot_list->create_item(root); TreeItem *t = template_source_list->create_item(root);
t->set_editable(0, false); t->set_editable(0, false);
t->set_text(0, pot_translations[i].replace_first("res://", "")); t->set_text(0, sources[i].replace_first("res://", ""));
t->set_tooltip_text(0, pot_translations[i]); t->set_tooltip_text(0, sources[i]);
t->set_metadata(0, i); t->set_metadata(0, i);
t->add_button(0, get_editor_theme_icon(SNAME("Remove")), 0, false, TTRC("Remove")); t->add_button(0, get_editor_theme_icon(SNAME("Remove")), 0, false, TTRC("Remove"));
} }
// New translation parser plugin might extend possible file extensions in POT generation. // New translation parser plugin might extend possible file extensions in template generation.
_update_pot_file_extensions(); _update_template_source_file_extensions();
pot_generate_button->set_disabled(pot_translations.is_empty()); template_generate_button->set_disabled(sources.is_empty() && !template_add_builtin->is_pressed());
updating_translations = false; updating_translations = false;
} }
@@ -844,7 +850,7 @@ LocalizationEditor::LocalizationEditor() {
{ {
VBoxContainer *tvb = memnew(VBoxContainer); VBoxContainer *tvb = memnew(VBoxContainer);
tvb->set_name(TTRC("POT Generation")); tvb->set_name(TTRC("Template Generation"));
translations->add_child(tvb); translations->add_child(tvb);
HBoxContainer *thb = memnew(HBoxContainer); HBoxContainer *thb = memnew(HBoxContainer);
@@ -855,35 +861,35 @@ LocalizationEditor::LocalizationEditor() {
tvb->add_child(thb); tvb->add_child(thb);
Button *addtr = memnew(Button(TTRC("Add..."))); Button *addtr = memnew(Button(TTRC("Add...")));
addtr->connect(SceneStringName(pressed), callable_mp(this, &LocalizationEditor::_pot_file_open)); addtr->connect(SceneStringName(pressed), callable_mp(this, &LocalizationEditor::_template_source_file_open));
thb->add_child(addtr); thb->add_child(addtr);
pot_generate_button = memnew(Button(TTRC("Generate POT"))); template_generate_button = memnew(Button(TTRC("Generate")));
pot_generate_button->connect(SceneStringName(pressed), callable_mp(this, &LocalizationEditor::_pot_generate_open)); template_generate_button->connect(SceneStringName(pressed), callable_mp(this, &LocalizationEditor::_template_generate_open));
thb->add_child(pot_generate_button); thb->add_child(template_generate_button);
translation_pot_list = memnew(Tree); template_source_list = memnew(Tree);
translation_pot_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); template_source_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
tvb->add_child(translation_pot_list); tvb->add_child(template_source_list);
trees.push_back(translation_pot_list); trees.push_back(template_source_list);
tree_data_types[translation_pot_list] = "localization_editor_pot_item"; tree_data_types[template_source_list] = "localization_editor_pot_item";
tree_settings[translation_pot_list] = "internationalization/locale/translations_pot_files"; tree_settings[template_source_list] = "internationalization/locale/translations_pot_files";
translation_pot_add_builtin = memnew(CheckBox(TTRC("Add Built-in Strings to POT"))); template_add_builtin = memnew(CheckBox(TTRC("Add Built-in Strings")));
translation_pot_add_builtin->set_tooltip_text(TTRC("Add strings from built-in components such as certain Control nodes.")); template_add_builtin->set_tooltip_text(TTRC("Add strings from built-in components such as certain Control nodes."));
translation_pot_add_builtin->connect(SceneStringName(pressed), callable_mp(this, &LocalizationEditor::_pot_add_builtin_toggled)); template_add_builtin->connect(SceneStringName(pressed), callable_mp(this, &LocalizationEditor::_template_add_builtin_toggled));
tvb->add_child(translation_pot_add_builtin); tvb->add_child(template_add_builtin);
pot_generate_dialog = memnew(EditorFileDialog); template_generate_dialog = memnew(EditorFileDialog);
pot_generate_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); template_generate_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
pot_generate_dialog->set_current_path(EditorSettings::get_singleton()->get_project_metadata("pot_generator", "last_pot_path", String())); template_generate_dialog->set_current_path(EditorSettings::get_singleton()->get_project_metadata("pot_generator", "last_pot_path", String()));
pot_generate_dialog->connect("file_selected", callable_mp(this, &LocalizationEditor::_pot_generate)); template_generate_dialog->connect("file_selected", callable_mp(this, &LocalizationEditor::_template_generate));
add_child(pot_generate_dialog); add_child(template_generate_dialog);
pot_file_open_dialog = memnew(EditorFileDialog); template_source_open_dialog = memnew(EditorFileDialog);
pot_file_open_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILES); template_source_open_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILES);
pot_file_open_dialog->connect("files_selected", callable_mp(this, &LocalizationEditor::_pot_add)); template_source_open_dialog->connect("files_selected", callable_mp(this, &LocalizationEditor::_template_source_add));
add_child(pot_file_open_dialog); add_child(template_source_open_dialog);
} }
for (Tree *tree : trees) { for (Tree *tree : trees) {

View File

@@ -51,11 +51,11 @@ class LocalizationEditor : public VBoxContainer {
Tree *translation_remap = nullptr; Tree *translation_remap = nullptr;
Tree *translation_remap_options = nullptr; Tree *translation_remap_options = nullptr;
Tree *translation_pot_list = nullptr; Tree *template_source_list = nullptr;
CheckBox *translation_pot_add_builtin = nullptr; CheckBox *template_add_builtin = nullptr;
EditorFileDialog *pot_file_open_dialog = nullptr; EditorFileDialog *template_source_open_dialog = nullptr;
EditorFileDialog *pot_generate_dialog = nullptr; EditorFileDialog *template_generate_dialog = nullptr;
Button *pot_generate_button = nullptr; Button *template_generate_button = nullptr;
bool updating_translations = false; bool updating_translations = false;
String localization_changed; String localization_changed;
@@ -79,13 +79,13 @@ class LocalizationEditor : public VBoxContainer {
void _translation_res_option_popup(bool p_arrow_clicked); void _translation_res_option_popup(bool p_arrow_clicked);
void _translation_res_option_selected(const String &p_locale); void _translation_res_option_selected(const String &p_locale);
void _pot_add(const PackedStringArray &p_paths); void _template_source_add(const PackedStringArray &p_paths);
void _pot_delete(Object *p_item, int p_column, int p_button, MouseButton p_mouse_button); void _template_source_delete(Object *p_item, int p_column, int p_button, MouseButton p_mouse_button);
void _pot_file_open(); void _template_source_file_open();
void _pot_generate_open(); void _template_generate_open();
void _pot_add_builtin_toggled(); void _template_add_builtin_toggled();
void _pot_generate(const String &p_file); void _template_generate(const String &p_file);
void _update_pot_file_extensions(); void _update_template_source_file_extensions();
void _filesystem_files_moved(const String &p_old_file, const String &p_new_file); void _filesystem_files_moved(const String &p_old_file, const String &p_new_file);
void _filesystem_file_removed(const String &p_file); void _filesystem_file_removed(const String &p_file);

View File

@@ -1,260 +0,0 @@
/**************************************************************************/
/* pot_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 "pot_generator.h"
#include "core/config/project_settings.h"
#include "core/error/error_macros.h"
#include "editor/translations/editor_translation.h"
#include "editor/translations/editor_translation_parser.h"
POTGenerator *POTGenerator::singleton = nullptr;
#ifdef DEBUG_POT
void POTGenerator::_print_all_translation_strings() {
for (HashMap<String, Vector<POTGenerator::MsgidData>>::Element E = all_translation_strings.front(); E; E = E.next()) {
Vector<MsgidData> v_md = all_translation_strings[E.key()];
for (int i = 0; i < v_md.size(); i++) {
print_line("++++++");
print_line("msgid: " + E.key());
print_line("context: " + v_md[i].ctx);
print_line("msgid_plural: " + v_md[i].plural);
for (const String &F : v_md[i].locations) {
print_line("location: " + F);
}
}
}
}
#endif
void POTGenerator::generate_pot(const String &p_file) {
Vector<String> files = GLOBAL_GET("internationalization/locale/translations_pot_files");
if (files.is_empty()) {
WARN_PRINT("No files selected for POT generation.");
return;
}
// Clear all_translation_strings of the previous round.
all_translation_strings.clear();
// Collect all translatable strings according to files order in "POT Generation" setting.
for (int i = 0; i < files.size(); i++) {
Vector<Vector<String>> translations;
const String &file_path = files[i];
String file_extension = file_path.get_extension();
if (EditorTranslationParser::get_singleton()->can_parse(file_extension)) {
EditorTranslationParser::get_singleton()->get_parser(file_extension)->parse_file(file_path, &translations);
} else {
ERR_PRINT("Unrecognized file extension " + file_extension + " in generate_pot()");
return;
}
for (const Vector<String> &translation : translations) {
ERR_CONTINUE(translation.is_empty());
const String &msgctxt = (translation.size() > 1) ? translation[1] : String();
const String &msgid_plural = (translation.size() > 2) ? translation[2] : String();
const String &comment = (translation.size() > 3) ? translation[3] : String();
const int source_line = (translation.size() > 4) ? translation[4].to_int() : 0;
String location = file_path;
if (source_line > 0) {
location += vformat(":%d", source_line);
}
_add_new_msgid(translation[0], msgctxt, msgid_plural, location, comment);
}
}
if (GLOBAL_GET("internationalization/locale/translation_add_builtin_strings_to_pot")) {
for (const Vector<String> &extractable_msgids : get_extractable_message_list()) {
_add_new_msgid(extractable_msgids[0], extractable_msgids[1], extractable_msgids[2], "", "");
}
}
_write_to_pot(p_file);
}
void POTGenerator::_write_to_pot(const String &p_file) {
Error err;
Ref<FileAccess> file = FileAccess::open(p_file, FileAccess::WRITE, &err);
if (err != OK) {
ERR_PRINT("Failed to open " + p_file);
return;
}
String project_name = GLOBAL_GET("application/config/name").operator String().replace("\n", "\\n");
Vector<String> files = GLOBAL_GET("internationalization/locale/translations_pot_files");
String extracted_files = "";
for (int i = 0; i < files.size(); i++) {
extracted_files += "# " + files[i].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";
file->store_string(header);
for (const KeyValue<String, Vector<MsgidData>> &E_pair : all_translation_strings) {
String msgid = E_pair.key;
const Vector<MsgidData> &v_msgid_data = E_pair.value;
for (int i = 0; i < v_msgid_data.size(); i++) {
String context = v_msgid_data[i].ctx;
String plural = v_msgid_data[i].plural;
const HashSet<String> &locations = v_msgid_data[i].locations;
const HashSet<String> &comments = v_msgid_data[i].comments;
// Put the blank line at the start, to avoid a double at the end when closing the file.
file->store_line("");
// Write comments.
bool is_first_comment = true;
for (const String &E : comments) {
if (is_first_comment) {
file->store_line("#. TRANSLATORS: " + E.replace("\n", "\n#. "));
} else {
file->store_line("#. " + E.replace("\n", "\n#. "));
}
is_first_comment = false;
}
// Write file locations.
for (const String &E : locations) {
file->store_line("#: " + E.trim_prefix("res://").replace("\n", "\\n"));
}
// Write context.
if (!context.is_empty()) {
file->store_line("msgctxt " + context.json_escape().quote());
}
// Write msgid.
_write_msgid(file, msgid, false);
// Write msgid_plural.
if (!plural.is_empty()) {
_write_msgid(file, plural, true);
file->store_line("msgstr[0] \"\"");
file->store_line("msgstr[1] \"\"");
} else {
file->store_line("msgstr \"\"");
}
}
}
}
void POTGenerator::_write_msgid(Ref<FileAccess> r_file, const String &p_id, bool p_plural) {
if (p_plural) {
r_file->store_string("msgid_plural ");
} else {
r_file->store_string("msgid ");
}
if (p_id.is_empty()) {
r_file->store_line("\"\"");
return;
}
const Vector<String> lines = p_id.split("\n");
const String &last_line = lines[lines.size() - 1]; // `lines` cannot be empty.
int pot_line_count = lines.size();
if (last_line.is_empty()) {
pot_line_count--;
}
if (pot_line_count > 1) {
r_file->store_line("\"\"");
}
for (int i = 0; i < lines.size() - 1; i++) {
r_file->store_line((lines[i] + "\n").json_escape().quote());
}
if (!last_line.is_empty()) {
r_file->store_line(last_line.json_escape().quote());
}
}
void POTGenerator::_add_new_msgid(const String &p_msgid, const String &p_context, const String &p_plural, const String &p_location, const String &p_comment) {
// Insert new location if msgid under same context exists already.
if (all_translation_strings.has(p_msgid)) {
Vector<MsgidData> &v_mdata = all_translation_strings[p_msgid];
for (int i = 0; i < v_mdata.size(); i++) {
if (v_mdata[i].ctx == p_context) {
if (!v_mdata[i].plural.is_empty() && !p_plural.is_empty() && v_mdata[i].plural != p_plural) {
WARN_PRINT("Redefinition of plural message (msgid_plural), under the same message (msgid) and context (msgctxt)");
}
if (!p_location.is_empty()) {
v_mdata.write[i].locations.insert(p_location);
}
if (!p_comment.is_empty()) {
v_mdata.write[i].comments.insert(p_comment);
}
return;
}
}
}
// Add a new entry.
MsgidData mdata;
mdata.ctx = p_context;
mdata.plural = p_plural;
if (!p_location.is_empty()) {
mdata.locations.insert(p_location);
}
if (!p_comment.is_empty()) {
mdata.comments.insert(p_comment);
}
all_translation_strings[p_msgid].push_back(mdata);
}
POTGenerator *POTGenerator::get_singleton() {
if (!singleton) {
singleton = memnew(POTGenerator);
}
return singleton;
}
POTGenerator::~POTGenerator() {
memdelete(singleton);
singleton = nullptr;
}

View File

@@ -0,0 +1,285 @@
/**************************************************************************/
/* 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());
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() });
}
}
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("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;
}

View File

@@ -1,5 +1,5 @@
/**************************************************************************/ /**************************************************************************/
/* pot_generator.h */ /* template_generator.h */
/**************************************************************************/ /**************************************************************************/
/* This file is part of: */ /* This file is part of: */
/* GODOT ENGINE */ /* GODOT ENGINE */
@@ -31,34 +31,28 @@
#pragma once #pragma once
#include "core/io/file_access.h" #include "core/io/file_access.h"
#include "core/templates/hash_map.h" #include "core/string/translation.h"
#include "core/templates/hash_set.h"
//#define DEBUG_POT class TranslationTemplateGenerator {
static inline TranslationTemplateGenerator *singleton = nullptr;
class POTGenerator { struct MessageData {
static POTGenerator *singleton;
struct MsgidData {
String ctx;
String plural; String plural;
HashSet<String> locations; HashSet<String> locations;
HashSet<String> comments; HashSet<String> comments;
}; };
// Store msgid as key and the additional data around the msgid - if it's under a context, has plurals and its file locations.
HashMap<String, Vector<MsgidData>> all_translation_strings;
void _write_to_pot(const String &p_file); using MessageMap = HashMap<Translation::MessageKey, MessageData, Translation::MessageKey>;
void _write_msgid(Ref<FileAccess> r_file, const String &p_id, bool p_plural);
void _add_new_msgid(const String &p_msgid, const String &p_context, const String &p_plural, const String &p_location, const String &p_comment);
#ifdef DEBUG_POT MessageMap parse(const Vector<String> &p_sources, bool p_add_builtin) const;
void _print_all_translation_strings();
#endif void _write_to_pot(Ref<FileAccess> p_file, const MessageMap &p_map) const;
void _write_to_csv(Ref<FileAccess> p_file, const MessageMap &p_map) const;
public: public:
static POTGenerator *get_singleton(); static TranslationTemplateGenerator *get_singleton();
void generate_pot(const String &p_file);
~POTGenerator(); void generate(const String &p_file);
~TranslationTemplateGenerator();
}; };