1/**************************************************************************/
2/* pot_generator.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "pot_generator.h"
32
33#include "core/config/project_settings.h"
34#include "core/error/error_macros.h"
35#include "editor/editor_translation_parser.h"
36#include "plugins/packed_scene_translation_parser_plugin.h"
37
38POTGenerator *POTGenerator::singleton = nullptr;
39
40#ifdef DEBUG_POT
41void POTGenerator::_print_all_translation_strings() {
42 for (HashMap<String, Vector<POTGenerator::MsgidData>>::Element E = all_translation_strings.front(); E; E = E.next()) {
43 Vector<MsgidData> v_md = all_translation_strings[E.key()];
44 for (int i = 0; i < v_md.size(); i++) {
45 print_line("++++++");
46 print_line("msgid: " + E.key());
47 print_line("context: " + v_md[i].ctx);
48 print_line("msgid_plural: " + v_md[i].plural);
49 for (const String &F : v_md[i].locations) {
50 print_line("location: " + F);
51 }
52 }
53 }
54}
55#endif
56
57void POTGenerator::generate_pot(const String &p_file) {
58 Vector<String> files = GLOBAL_GET("internationalization/locale/translations_pot_files");
59
60 if (files.is_empty()) {
61 WARN_PRINT("No files selected for POT generation.");
62 return;
63 }
64
65 // Clear all_translation_strings of the previous round.
66 all_translation_strings.clear();
67
68 // Collect all translatable strings according to files order in "POT Generation" setting.
69 for (int i = 0; i < files.size(); i++) {
70 Vector<String> msgids;
71 Vector<Vector<String>> msgids_context_plural;
72 String file_path = files[i];
73 String file_extension = file_path.get_extension();
74
75 if (EditorTranslationParser::get_singleton()->can_parse(file_extension)) {
76 EditorTranslationParser::get_singleton()->get_parser(file_extension)->parse_file(file_path, &msgids, &msgids_context_plural);
77 } else {
78 ERR_PRINT("Unrecognized file extension " + file_extension + " in generate_pot()");
79 return;
80 }
81
82 for (int j = 0; j < msgids_context_plural.size(); j++) {
83 Vector<String> entry = msgids_context_plural[j];
84 _add_new_msgid(entry[0], entry[1], entry[2], file_path);
85 }
86 for (int j = 0; j < msgids.size(); j++) {
87 _add_new_msgid(msgids[j], "", "", file_path);
88 }
89 }
90
91 _write_to_pot(p_file);
92}
93
94void POTGenerator::_write_to_pot(const String &p_file) {
95 Error err;
96 Ref<FileAccess> file = FileAccess::open(p_file, FileAccess::WRITE, &err);
97 if (err != OK) {
98 ERR_PRINT("Failed to open " + p_file);
99 return;
100 }
101
102 String project_name = GLOBAL_GET("application/config/name").operator String().replace("\n", "\\n");
103 Vector<String> files = GLOBAL_GET("internationalization/locale/translations_pot_files");
104 String extracted_files = "";
105 for (int i = 0; i < files.size(); i++) {
106 extracted_files += "# " + files[i].replace("\n", "\\n") + "\n";
107 }
108 const String header =
109 "# LANGUAGE translation for " + project_name + " for the following files:\n" +
110 extracted_files +
111 "#\n"
112 "# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n"
113 "#\n"
114 "#, fuzzy\n"
115 "msgid \"\"\n"
116 "msgstr \"\"\n"
117 "\"Project-Id-Version: " +
118 project_name +
119 "\\n\"\n"
120 "\"MIME-Version: 1.0\\n\"\n"
121 "\"Content-Type: text/plain; charset=UTF-8\\n\"\n"
122 "\"Content-Transfer-Encoding: 8-bit\\n\"\n";
123
124 file->store_string(header);
125
126 for (const KeyValue<String, Vector<MsgidData>> &E_pair : all_translation_strings) {
127 String msgid = E_pair.key;
128 const Vector<MsgidData> &v_msgid_data = E_pair.value;
129 for (int i = 0; i < v_msgid_data.size(); i++) {
130 String context = v_msgid_data[i].ctx;
131 String plural = v_msgid_data[i].plural;
132 const HashSet<String> &locations = v_msgid_data[i].locations;
133
134 // Put the blank line at the start, to avoid a double at the end when closing the file.
135 file->store_line("");
136
137 // Write file locations.
138 for (const String &E : locations) {
139 file->store_line("#: " + E.trim_prefix("res://").replace("\n", "\\n"));
140 }
141
142 // Write context.
143 if (!context.is_empty()) {
144 file->store_line("msgctxt " + context.c_escape().quote());
145 }
146
147 // Write msgid.
148 _write_msgid(file, msgid, false);
149
150 // Write msgid_plural.
151 if (!plural.is_empty()) {
152 _write_msgid(file, plural, true);
153 file->store_line("msgstr[0] \"\"");
154 file->store_line("msgstr[1] \"\"");
155 } else {
156 file->store_line("msgstr \"\"");
157 }
158 }
159 }
160}
161
162void POTGenerator::_write_msgid(Ref<FileAccess> r_file, const String &p_id, bool p_plural) {
163 if (p_plural) {
164 r_file->store_string("msgid_plural ");
165 } else {
166 r_file->store_string("msgid ");
167 }
168
169 if (p_id.is_empty()) {
170 r_file->store_line("\"\"");
171 return;
172 }
173
174 const Vector<String> lines = p_id.split("\n");
175 const String &last_line = lines[lines.size() - 1]; // `lines` cannot be empty.
176 int pot_line_count = lines.size();
177 if (last_line.is_empty()) {
178 pot_line_count--;
179 }
180
181 if (pot_line_count > 1) {
182 r_file->store_line("\"\"");
183 }
184
185 for (int i = 0; i < lines.size() - 1; i++) {
186 r_file->store_line((lines[i] + "\n").c_escape().quote());
187 }
188
189 if (!last_line.is_empty()) {
190 r_file->store_line(last_line.c_escape().quote());
191 }
192}
193
194void POTGenerator::_add_new_msgid(const String &p_msgid, const String &p_context, const String &p_plural, const String &p_location) {
195 // Insert new location if msgid under same context exists already.
196 if (all_translation_strings.has(p_msgid)) {
197 Vector<MsgidData> &v_mdata = all_translation_strings[p_msgid];
198 for (int i = 0; i < v_mdata.size(); i++) {
199 if (v_mdata[i].ctx == p_context) {
200 if (!v_mdata[i].plural.is_empty() && !p_plural.is_empty() && v_mdata[i].plural != p_plural) {
201 WARN_PRINT("Redefinition of plural message (msgid_plural), under the same message (msgid) and context (msgctxt)");
202 }
203 v_mdata.write[i].locations.insert(p_location);
204 return;
205 }
206 }
207 }
208
209 // Add a new entry of msgid, context, plural and location - context and plural might be empty if the inserted msgid doesn't associated
210 // context or plurals.
211 MsgidData mdata;
212 mdata.ctx = p_context;
213 mdata.plural = p_plural;
214 mdata.locations.insert(p_location);
215 all_translation_strings[p_msgid].push_back(mdata);
216}
217
218POTGenerator *POTGenerator::get_singleton() {
219 if (!singleton) {
220 singleton = memnew(POTGenerator);
221 }
222 return singleton;
223}
224
225POTGenerator::POTGenerator() {
226}
227
228POTGenerator::~POTGenerator() {
229 memdelete(singleton);
230 singleton = nullptr;
231}
232