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 | |
38 | POTGenerator *POTGenerator::singleton = nullptr; |
39 | |
40 | #ifdef DEBUG_POT |
41 | void 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 | |
57 | void 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 | |
94 | void 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 = "" ; |
105 | for (int i = 0; i < files.size(); i++) { |
106 | extracted_files += "# " + files[i].replace("\n" , "\\n" ) + "\n" ; |
107 | } |
108 | const String = |
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 | |
162 | void 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 | |
194 | void 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 | |
218 | POTGenerator *POTGenerator::get_singleton() { |
219 | if (!singleton) { |
220 | singleton = memnew(POTGenerator); |
221 | } |
222 | return singleton; |
223 | } |
224 | |
225 | POTGenerator::POTGenerator() { |
226 | } |
227 | |
228 | POTGenerator::~POTGenerator() { |
229 | memdelete(singleton); |
230 | singleton = nullptr; |
231 | } |
232 | |