| 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 | |