Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/editor/translations/template_generator.cpp
14709 views
1
/**************************************************************************/
2
/* template_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 "template_generator.h"
32
33
#include "core/config/project_settings.h"
34
#include "editor/translations/editor_translation.h"
35
#include "editor/translations/editor_translation_parser.h"
36
37
TranslationTemplateGenerator::MessageMap TranslationTemplateGenerator::parse(const Vector<String> &p_sources, bool p_add_builtin) const {
38
Vector<Vector<String>> raw;
39
40
for (const String &path : p_sources) {
41
Vector<Vector<String>> parsed_from_file;
42
43
const String &extension = path.get_extension();
44
ERR_CONTINUE_MSG(!EditorTranslationParser::get_singleton()->can_parse(extension), vformat("Cannot parse file '%s': unrecognized file extension. Skipping.", path));
45
46
EditorTranslationParser::get_singleton()->get_parser(extension)->parse_file(path, &parsed_from_file);
47
48
for (const Vector<String> &entry : parsed_from_file) {
49
ERR_CONTINUE(entry.is_empty());
50
if (entry[0].is_empty()) {
51
continue;
52
}
53
54
const String &msgctxt = (entry.size() > 1) ? entry[1] : String();
55
const String &msgid_plural = (entry.size() > 2) ? entry[2] : String();
56
const String &comment = (entry.size() > 3) ? entry[3] : String();
57
const int source_line = (entry.size() > 4) ? entry[4].to_int() : 0;
58
const String &location = source_line > 0 ? vformat("%s:%d", path, source_line) : path;
59
60
raw.push_back({ entry[0], msgctxt, msgid_plural, comment, location });
61
}
62
}
63
64
if (p_add_builtin) {
65
for (const Vector<String> &extractable_msgids : get_extractable_message_list()) {
66
raw.push_back({ extractable_msgids[0], extractable_msgids[1], extractable_msgids[2], String(), String() });
67
}
68
}
69
70
if (GLOBAL_GET("application/config/name_localized").operator Dictionary().is_empty()) {
71
const String &project_name = GLOBAL_GET("application/config/name");
72
if (!project_name.is_empty()) {
73
raw.push_back({ project_name, String(), String(), String(), String() });
74
}
75
}
76
77
MessageMap result;
78
for (const Vector<String> &entry : raw) {
79
const String &msgid = entry[0];
80
const String &msgctxt = entry[1];
81
const String &plural = entry[2];
82
const String &comment = entry[3];
83
const String &location = entry[4];
84
85
const Translation::MessageKey key = { msgctxt, msgid };
86
MessageData &mdata = result[key];
87
if (!mdata.plural.is_empty() && !plural.is_empty() && mdata.plural != plural) {
88
WARN_PRINT(vformat(R"(Skipping different plural definitions for msgid "%s" msgctxt "%s": "%s" and "%s")", msgid, msgctxt, mdata.plural, plural));
89
continue;
90
}
91
mdata.plural = plural;
92
if (!location.is_empty()) {
93
mdata.locations.insert(location);
94
}
95
if (!comment.is_empty()) {
96
mdata.comments.insert(comment);
97
}
98
}
99
return result;
100
}
101
102
void TranslationTemplateGenerator::generate(const String &p_file) {
103
const Vector<String> files = GLOBAL_GET("internationalization/locale/translations_pot_files");
104
const bool add_builtin = GLOBAL_GET("internationalization/locale/translation_add_builtin_strings_to_pot");
105
106
const MessageMap &map = parse(files, add_builtin);
107
if (map.is_empty()) {
108
WARN_PRINT_ED(TTR("No translatable strings found."));
109
return;
110
}
111
112
Error err;
113
Ref<FileAccess> file = FileAccess::open(p_file, FileAccess::WRITE, &err);
114
ERR_FAIL_COND_MSG(err != OK, "Failed to open " + p_file);
115
116
const String ext = p_file.get_extension().to_lower();
117
if (ext == "pot") {
118
_write_to_pot(file, map);
119
} else if (ext == "csv") {
120
_write_to_csv(file, map);
121
} else {
122
ERR_FAIL_MSG("Unrecognized translation template file extension: " + ext);
123
}
124
}
125
126
static void _write_pot_field(Ref<FileAccess> p_file, const String &p_name, const String &p_value) {
127
p_file->store_string(p_name + " ");
128
129
if (p_value.is_empty()) {
130
p_file->store_line("\"\"");
131
return;
132
}
133
134
const Vector<String> lines = p_value.split("\n");
135
DEV_ASSERT(lines.size() > 0);
136
137
const String &last_line = lines[lines.size() - 1];
138
const int pot_line_count = last_line.is_empty() ? lines.size() - 1 : lines.size();
139
140
if (pot_line_count > 1) {
141
p_file->store_line("\"\"");
142
}
143
144
for (int i = 0; i < lines.size() - 1; i++) {
145
p_file->store_line((lines[i] + "\n").json_escape().quote());
146
}
147
if (!last_line.is_empty()) {
148
p_file->store_line(last_line.json_escape().quote());
149
}
150
}
151
152
void TranslationTemplateGenerator::_write_to_pot(Ref<FileAccess> p_file, const MessageMap &p_map) const {
153
const String project_name = GLOBAL_GET("application/config/name").operator String().replace("\n", "\\n");
154
const Vector<String> files = GLOBAL_GET("internationalization/locale/translations_pot_files");
155
String extracted_files;
156
for (const String &file : files) {
157
extracted_files += "# " + file.replace("\n", "\\n") + "\n";
158
}
159
const String header =
160
"# LANGUAGE translation for " + project_name + " for the following files:\n" +
161
extracted_files +
162
"#\n"
163
"# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n"
164
"#\n"
165
"#, fuzzy\n"
166
"msgid \"\"\n"
167
"msgstr \"\"\n"
168
"\"Project-Id-Version: " +
169
project_name +
170
"\\n\"\n"
171
"\"MIME-Version: 1.0\\n\"\n"
172
"\"Content-Type: text/plain; charset=UTF-8\\n\"\n"
173
"\"Content-Transfer-Encoding: 8-bit\\n\"\n";
174
p_file->store_string(header);
175
176
for (const KeyValue<Translation::MessageKey, MessageData> &E : p_map) {
177
// Put the blank line at the start, to avoid a double at the end when closing the file.
178
p_file->store_line("");
179
180
// Write comments.
181
bool is_first_comment = true;
182
for (const String &comment : E.value.comments) {
183
if (is_first_comment) {
184
p_file->store_line("#. TRANSLATORS: " + comment.replace("\n", "\n#. "));
185
} else {
186
p_file->store_line("#. " + comment.replace("\n", "\n#. "));
187
}
188
is_first_comment = false;
189
}
190
191
// Write file locations.
192
for (const String &location : E.value.locations) {
193
p_file->store_line("#: " + location.trim_prefix("res://").replace("\n", "\\n"));
194
}
195
196
// Write context.
197
const String msgctxt = E.key.msgctxt;
198
if (!msgctxt.is_empty()) {
199
p_file->store_line("msgctxt " + msgctxt.json_escape().quote());
200
}
201
202
// Write msgid.
203
_write_pot_field(p_file, "msgid", E.key.msgid);
204
205
// Write msgid_plural.
206
if (E.value.plural.is_empty()) {
207
p_file->store_line("msgstr \"\"");
208
} else {
209
_write_pot_field(p_file, "msgid_plural", E.value.plural);
210
p_file->store_line("msgstr[0] \"\"");
211
p_file->store_line("msgstr[1] \"\"");
212
}
213
}
214
}
215
216
static String _join_strings(const HashSet<String> &p_strings) {
217
String result;
218
bool is_first = true;
219
for (const String &s : p_strings) {
220
if (!is_first) {
221
result += '\n';
222
}
223
result += s;
224
is_first = false;
225
}
226
return result;
227
}
228
229
void TranslationTemplateGenerator::_write_to_csv(Ref<FileAccess> p_file, const MessageMap &p_map) const {
230
// Avoid adding unnecessary columns.
231
bool context_used = false;
232
bool plural_used = false;
233
bool comments_used = false;
234
bool locations_used = false;
235
{
236
for (const KeyValue<Translation::MessageKey, MessageData> &E : p_map) {
237
if (!context_used && !E.key.msgctxt.is_empty()) {
238
context_used = true;
239
}
240
if (!plural_used && !E.value.plural.is_empty()) {
241
plural_used = true;
242
}
243
if (!comments_used && !E.value.comments.is_empty()) {
244
comments_used = true;
245
}
246
if (!locations_used && !E.value.locations.is_empty()) {
247
locations_used = true;
248
}
249
}
250
}
251
252
Vector<String> header = { "key" };
253
if (context_used) {
254
header.push_back("?context");
255
}
256
if (plural_used) {
257
header.push_back("?plural");
258
}
259
if (comments_used) {
260
header.push_back("_comments");
261
}
262
if (locations_used) {
263
header.push_back("_locations");
264
}
265
p_file->store_csv_line(header);
266
267
for (const KeyValue<Translation::MessageKey, MessageData> &E : p_map) {
268
Vector<String> line = { E.key.msgid };
269
if (context_used) {
270
line.push_back(E.key.msgctxt);
271
}
272
if (plural_used) {
273
line.push_back(E.value.plural);
274
}
275
if (comments_used) {
276
line.push_back(_join_strings(E.value.comments));
277
}
278
if (locations_used) {
279
line.push_back(_join_strings(E.value.locations));
280
}
281
p_file->store_csv_line(line);
282
}
283
}
284
285
TranslationTemplateGenerator *TranslationTemplateGenerator::get_singleton() {
286
if (!singleton) {
287
singleton = memnew(TranslationTemplateGenerator);
288
}
289
return singleton;
290
}
291
292
TranslationTemplateGenerator::~TranslationTemplateGenerator() {
293
memdelete(singleton);
294
singleton = nullptr;
295
}
296
297