1/**************************************************************************/
2/* localization_editor.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 "localization_editor.h"
32
33#include "core/config/project_settings.h"
34#include "core/string/translation.h"
35#include "editor/editor_scale.h"
36#include "editor/editor_translation_parser.h"
37#include "editor/editor_undo_redo_manager.h"
38#include "editor/filesystem_dock.h"
39#include "editor/gui/editor_file_dialog.h"
40#include "editor/pot_generator.h"
41#include "scene/gui/control.h"
42
43void LocalizationEditor::_notification(int p_what) {
44 switch (p_what) {
45 case NOTIFICATION_ENTER_TREE: {
46 translation_list->connect("button_clicked", callable_mp(this, &LocalizationEditor::_translation_delete));
47 translation_pot_list->connect("button_clicked", callable_mp(this, &LocalizationEditor::_pot_delete));
48
49 List<String> tfn;
50 ResourceLoader::get_recognized_extensions_for_type("Translation", &tfn);
51 for (const String &E : tfn) {
52 translation_file_open->add_filter("*." + E);
53 }
54
55 List<String> rfn;
56 ResourceLoader::get_recognized_extensions_for_type("Resource", &rfn);
57 for (const String &E : rfn) {
58 translation_res_file_open_dialog->add_filter("*." + E);
59 translation_res_option_file_open_dialog->add_filter("*." + E);
60 }
61
62 _update_pot_file_extensions();
63 pot_generate_dialog->add_filter("*.pot");
64 } break;
65 }
66}
67
68void LocalizationEditor::add_translation(const String &p_translation) {
69 PackedStringArray translations;
70 translations.push_back(p_translation);
71 _translation_add(translations);
72}
73
74void LocalizationEditor::_translation_add(const PackedStringArray &p_paths) {
75 PackedStringArray translations = GLOBAL_GET("internationalization/locale/translations");
76 for (int i = 0; i < p_paths.size(); i++) {
77 if (!translations.has(p_paths[i])) {
78 // Don't add duplicate translation paths.
79 translations.push_back(p_paths[i]);
80 }
81 }
82
83 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
84 undo_redo->create_action(vformat(TTR("Add %d Translations"), p_paths.size()));
85 undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translations", translations);
86 undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translations", GLOBAL_GET("internationalization/locale/translations"));
87 undo_redo->add_do_method(this, "update_translations");
88 undo_redo->add_undo_method(this, "update_translations");
89 undo_redo->add_do_method(this, "emit_signal", localization_changed);
90 undo_redo->add_undo_method(this, "emit_signal", localization_changed);
91 undo_redo->commit_action();
92}
93
94void LocalizationEditor::_translation_file_open() {
95 translation_file_open->popup_file_dialog();
96}
97
98void LocalizationEditor::_translation_delete(Object *p_item, int p_column, int p_button, MouseButton p_mouse_button) {
99 if (p_mouse_button != MouseButton::LEFT) {
100 return;
101 }
102
103 TreeItem *ti = Object::cast_to<TreeItem>(p_item);
104 ERR_FAIL_NULL(ti);
105
106 int idx = ti->get_metadata(0);
107
108 PackedStringArray translations = GLOBAL_GET("internationalization/locale/translations");
109
110 ERR_FAIL_INDEX(idx, translations.size());
111
112 translations.remove_at(idx);
113
114 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
115 undo_redo->create_action(TTR("Remove Translation"));
116 undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translations", translations);
117 undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translations", GLOBAL_GET("internationalization/locale/translations"));
118 undo_redo->add_do_method(this, "update_translations");
119 undo_redo->add_undo_method(this, "update_translations");
120 undo_redo->add_do_method(this, "emit_signal", localization_changed);
121 undo_redo->add_undo_method(this, "emit_signal", localization_changed);
122 undo_redo->commit_action();
123}
124
125void LocalizationEditor::_translation_res_file_open() {
126 translation_res_file_open_dialog->popup_file_dialog();
127}
128
129void LocalizationEditor::_translation_res_add(const PackedStringArray &p_paths) {
130 Variant prev;
131 Dictionary remaps;
132
133 if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps")) {
134 remaps = GLOBAL_GET("internationalization/locale/translation_remaps");
135 prev = remaps;
136 }
137
138 for (int i = 0; i < p_paths.size(); i++) {
139 if (!remaps.has(p_paths[i])) {
140 // Don't overwrite with an empty remap array if an array already exists for the given path.
141 remaps[p_paths[i]] = PackedStringArray();
142 }
143 }
144
145 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
146 undo_redo->create_action(vformat(TTR("Translation Resource Remap: Add %d Path(s)"), p_paths.size()));
147 undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translation_remaps", remaps);
148 undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translation_remaps", prev);
149 undo_redo->add_do_method(this, "update_translations");
150 undo_redo->add_undo_method(this, "update_translations");
151 undo_redo->add_do_method(this, "emit_signal", localization_changed);
152 undo_redo->add_undo_method(this, "emit_signal", localization_changed);
153 undo_redo->commit_action();
154}
155
156void LocalizationEditor::_translation_res_option_file_open() {
157 translation_res_option_file_open_dialog->popup_file_dialog();
158}
159
160void LocalizationEditor::_translation_res_option_add(const PackedStringArray &p_paths) {
161 ERR_FAIL_COND(!ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps"));
162
163 Dictionary remaps = GLOBAL_GET("internationalization/locale/translation_remaps");
164
165 TreeItem *k = translation_remap->get_selected();
166 ERR_FAIL_NULL(k);
167
168 String key = k->get_metadata(0);
169
170 ERR_FAIL_COND(!remaps.has(key));
171 PackedStringArray r = remaps[key];
172 for (int i = 0; i < p_paths.size(); i++) {
173 r.push_back(p_paths[i] + ":" + "en");
174 }
175 remaps[key] = r;
176
177 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
178 undo_redo->create_action(vformat(TTR("Translation Resource Remap: Add %d Remap(s)"), p_paths.size()));
179 undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translation_remaps", remaps);
180 undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translation_remaps", GLOBAL_GET("internationalization/locale/translation_remaps"));
181 undo_redo->add_do_method(this, "update_translations");
182 undo_redo->add_undo_method(this, "update_translations");
183 undo_redo->add_do_method(this, "emit_signal", localization_changed);
184 undo_redo->add_undo_method(this, "emit_signal", localization_changed);
185 undo_redo->commit_action();
186}
187
188void LocalizationEditor::_translation_res_select() {
189 if (updating_translations) {
190 return;
191 }
192 call_deferred(SNAME("update_translations"));
193}
194
195void LocalizationEditor::_translation_res_option_popup(bool p_arrow_clicked) {
196 TreeItem *ed = translation_remap_options->get_edited();
197 ERR_FAIL_NULL(ed);
198
199 locale_select->set_locale(ed->get_tooltip_text(1));
200 locale_select->popup_locale_dialog();
201}
202
203void LocalizationEditor::_translation_res_option_selected(const String &p_locale) {
204 TreeItem *ed = translation_remap_options->get_edited();
205 ERR_FAIL_NULL(ed);
206
207 ed->set_text(1, TranslationServer::get_singleton()->get_locale_name(p_locale));
208 ed->set_tooltip_text(1, p_locale);
209
210 LocalizationEditor::_translation_res_option_changed();
211}
212
213void LocalizationEditor::_translation_res_option_changed() {
214 if (updating_translations) {
215 return;
216 }
217
218 if (!ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps")) {
219 return;
220 }
221
222 Dictionary remaps = GLOBAL_GET("internationalization/locale/translation_remaps");
223
224 TreeItem *k = translation_remap->get_selected();
225 ERR_FAIL_NULL(k);
226 TreeItem *ed = translation_remap_options->get_edited();
227 ERR_FAIL_NULL(ed);
228
229 String key = k->get_metadata(0);
230 int idx = ed->get_metadata(0);
231 String path = ed->get_metadata(1);
232 String locale = ed->get_tooltip_text(1);
233
234 ERR_FAIL_COND(!remaps.has(key));
235 PackedStringArray r = remaps[key];
236 r.set(idx, path + ":" + locale);
237 remaps[key] = r;
238
239 updating_translations = true;
240
241 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
242 undo_redo->create_action(TTR("Change Resource Remap Language"));
243 undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translation_remaps", remaps);
244 undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translation_remaps", GLOBAL_GET("internationalization/locale/translation_remaps"));
245 undo_redo->add_do_method(this, "update_translations");
246 undo_redo->add_undo_method(this, "update_translations");
247 undo_redo->add_do_method(this, "emit_signal", localization_changed);
248 undo_redo->add_undo_method(this, "emit_signal", localization_changed);
249 undo_redo->commit_action();
250 updating_translations = false;
251}
252
253void LocalizationEditor::_translation_res_delete(Object *p_item, int p_column, int p_button, MouseButton p_mouse_button) {
254 if (updating_translations) {
255 return;
256 }
257
258 if (p_mouse_button != MouseButton::LEFT) {
259 return;
260 }
261
262 if (!ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps")) {
263 return;
264 }
265
266 Dictionary remaps = GLOBAL_GET("internationalization/locale/translation_remaps");
267
268 TreeItem *k = Object::cast_to<TreeItem>(p_item);
269
270 String key = k->get_metadata(0);
271 ERR_FAIL_COND(!remaps.has(key));
272
273 remaps.erase(key);
274
275 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
276 undo_redo->create_action(TTR("Remove Resource Remap"));
277 undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translation_remaps", remaps);
278 undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translation_remaps", GLOBAL_GET("internationalization/locale/translation_remaps"));
279 undo_redo->add_do_method(this, "update_translations");
280 undo_redo->add_undo_method(this, "update_translations");
281 undo_redo->add_do_method(this, "emit_signal", localization_changed);
282 undo_redo->add_undo_method(this, "emit_signal", localization_changed);
283 undo_redo->commit_action();
284}
285
286void LocalizationEditor::_translation_res_option_delete(Object *p_item, int p_column, int p_button, MouseButton p_mouse_button) {
287 if (updating_translations) {
288 return;
289 }
290
291 if (p_mouse_button != MouseButton::LEFT) {
292 return;
293 }
294
295 if (!ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps")) {
296 return;
297 }
298
299 Dictionary remaps = GLOBAL_GET("internationalization/locale/translation_remaps");
300
301 TreeItem *k = translation_remap->get_selected();
302 ERR_FAIL_NULL(k);
303 TreeItem *ed = Object::cast_to<TreeItem>(p_item);
304 ERR_FAIL_NULL(ed);
305
306 String key = k->get_metadata(0);
307 int idx = ed->get_metadata(0);
308
309 ERR_FAIL_COND(!remaps.has(key));
310 PackedStringArray r = remaps[key];
311 ERR_FAIL_INDEX(idx, r.size());
312 r.remove_at(idx);
313 remaps[key] = r;
314
315 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
316 undo_redo->create_action(TTR("Remove Resource Remap Option"));
317 undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translation_remaps", remaps);
318 undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translation_remaps", GLOBAL_GET("internationalization/locale/translation_remaps"));
319 undo_redo->add_do_method(this, "update_translations");
320 undo_redo->add_undo_method(this, "update_translations");
321 undo_redo->add_do_method(this, "emit_signal", localization_changed);
322 undo_redo->add_undo_method(this, "emit_signal", localization_changed);
323 undo_redo->commit_action();
324}
325
326void LocalizationEditor::_pot_add(const PackedStringArray &p_paths) {
327 PackedStringArray pot_translations = GLOBAL_GET("internationalization/locale/translations_pot_files");
328 for (int i = 0; i < p_paths.size(); i++) {
329 if (!pot_translations.has(p_paths[i])) {
330 pot_translations.push_back(p_paths[i]);
331 }
332 }
333
334 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
335 undo_redo->create_action(vformat(TTR("Add %d file(s) for POT generation"), p_paths.size()));
336 undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", pot_translations);
337 undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", GLOBAL_GET("internationalization/locale/translations_pot_files"));
338 undo_redo->add_do_method(this, "update_translations");
339 undo_redo->add_undo_method(this, "update_translations");
340 undo_redo->add_do_method(this, "emit_signal", localization_changed);
341 undo_redo->add_undo_method(this, "emit_signal", localization_changed);
342 undo_redo->commit_action();
343}
344
345void LocalizationEditor::_pot_delete(Object *p_item, int p_column, int p_button, MouseButton p_mouse_button) {
346 if (p_mouse_button != MouseButton::LEFT) {
347 return;
348 }
349
350 TreeItem *ti = Object::cast_to<TreeItem>(p_item);
351 ERR_FAIL_NULL(ti);
352
353 int idx = ti->get_metadata(0);
354
355 PackedStringArray pot_translations = GLOBAL_GET("internationalization/locale/translations_pot_files");
356
357 ERR_FAIL_INDEX(idx, pot_translations.size());
358
359 pot_translations.remove_at(idx);
360
361 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
362 undo_redo->create_action(TTR("Remove file from POT generation"));
363 undo_redo->add_do_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", pot_translations);
364 undo_redo->add_undo_property(ProjectSettings::get_singleton(), "internationalization/locale/translations_pot_files", GLOBAL_GET("internationalization/locale/translations_pot_files"));
365 undo_redo->add_do_method(this, "update_translations");
366 undo_redo->add_undo_method(this, "update_translations");
367 undo_redo->add_do_method(this, "emit_signal", localization_changed);
368 undo_redo->add_undo_method(this, "emit_signal", localization_changed);
369 undo_redo->commit_action();
370}
371
372void LocalizationEditor::_pot_file_open() {
373 pot_file_open_dialog->popup_file_dialog();
374}
375
376void LocalizationEditor::_pot_generate_open() {
377 pot_generate_dialog->popup_file_dialog();
378}
379
380void LocalizationEditor::_pot_generate(const String &p_file) {
381 POTGenerator::get_singleton()->generate_pot(p_file);
382}
383
384void LocalizationEditor::_update_pot_file_extensions() {
385 pot_file_open_dialog->clear_filters();
386 List<String> translation_parse_file_extensions;
387 EditorTranslationParser::get_singleton()->get_recognized_extensions(&translation_parse_file_extensions);
388 for (const String &E : translation_parse_file_extensions) {
389 pot_file_open_dialog->add_filter("*." + E);
390 }
391}
392
393void LocalizationEditor::connect_filesystem_dock_signals(FileSystemDock *p_fs_dock) {
394 p_fs_dock->connect("files_moved", callable_mp(this, &LocalizationEditor::_filesystem_files_moved));
395 p_fs_dock->connect("file_removed", callable_mp(this, &LocalizationEditor::_filesystem_file_removed));
396}
397
398void LocalizationEditor::_filesystem_files_moved(const String &p_old_file, const String &p_new_file) {
399 // Update remaps if the moved file is a part of them.
400 Dictionary remaps;
401 bool remaps_changed = false;
402
403 if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps")) {
404 remaps = GLOBAL_GET("internationalization/locale/translation_remaps");
405 }
406
407 // Check for the keys.
408 if (remaps.has(p_old_file)) {
409 PackedStringArray remapped_files = remaps[p_old_file];
410 remaps.erase(p_old_file);
411 remaps[p_new_file] = remapped_files;
412 remaps_changed = true;
413 print_verbose(vformat("Changed remap key \"%s\" to \"%s\" due to a moved file.", p_old_file, p_new_file));
414 }
415
416 // Check for the Array elements of the values.
417 Array remap_keys = remaps.keys();
418 for (int i = 0; i < remap_keys.size(); i++) {
419 PackedStringArray remapped_files = remaps[remap_keys[i]];
420 bool remapped_files_updated = false;
421
422 for (int j = 0; j < remapped_files.size(); j++) {
423 int splitter_pos = remapped_files[j].rfind(":");
424 String res_path = remapped_files[j].substr(0, splitter_pos);
425
426 if (res_path == p_old_file) {
427 String locale_name = remapped_files[j].substr(splitter_pos + 1);
428 // Replace the element at that index.
429 remapped_files.insert(j, p_new_file + ":" + locale_name);
430 remapped_files.remove_at(j + 1);
431 remaps_changed = true;
432 remapped_files_updated = true;
433 print_verbose(vformat("Changed remap value \"%s\" to \"%s\" of key \"%s\" due to a moved file.", res_path + ":" + locale_name, remapped_files[j], remap_keys[i]));
434 }
435 }
436
437 if (remapped_files_updated) {
438 remaps[remap_keys[i]] = remapped_files;
439 }
440 }
441
442 if (remaps_changed) {
443 ProjectSettings::get_singleton()->set_setting("internationalization/locale/translation_remaps", remaps);
444 update_translations();
445 emit_signal("localization_changed");
446 }
447}
448
449void LocalizationEditor::_filesystem_file_removed(const String &p_file) {
450 // Check if the remaps are affected.
451 Dictionary remaps;
452
453 if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps")) {
454 remaps = GLOBAL_GET("internationalization/locale/translation_remaps");
455 }
456
457 bool remaps_changed = remaps.has(p_file);
458
459 if (!remaps_changed) {
460 Array remap_keys = remaps.keys();
461 for (int i = 0; i < remap_keys.size() && !remaps_changed; i++) {
462 PackedStringArray remapped_files = remaps[remap_keys[i]];
463 for (int j = 0; j < remapped_files.size() && !remaps_changed; j++) {
464 int splitter_pos = remapped_files[j].rfind(":");
465 String res_path = remapped_files[j].substr(0, splitter_pos);
466 remaps_changed = p_file == res_path;
467 if (remaps_changed) {
468 print_verbose(vformat("Remap value \"%s\" of key \"%s\" has been removed from the file system.", remapped_files[j], remap_keys[i]));
469 }
470 }
471 }
472 } else {
473 print_verbose(vformat("Remap key \"%s\" has been removed from the file system.", p_file));
474 }
475
476 if (remaps_changed) {
477 update_translations();
478 emit_signal("localization_changed");
479 }
480}
481
482void LocalizationEditor::update_translations() {
483 if (updating_translations) {
484 return;
485 }
486
487 updating_translations = true;
488
489 translation_list->clear();
490 TreeItem *root = translation_list->create_item(nullptr);
491 translation_list->set_hide_root(true);
492 if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/translations")) {
493 PackedStringArray translations = GLOBAL_GET("internationalization/locale/translations");
494 for (int i = 0; i < translations.size(); i++) {
495 TreeItem *t = translation_list->create_item(root);
496 t->set_editable(0, false);
497 t->set_text(0, translations[i].replace_first("res://", ""));
498 t->set_tooltip_text(0, translations[i]);
499 t->set_metadata(0, i);
500 t->add_button(0, get_editor_theme_icon(SNAME("Remove")), 0, false, TTR("Remove"));
501 }
502 }
503
504 // Update translation remaps.
505 String remap_selected;
506 if (translation_remap->get_selected()) {
507 remap_selected = translation_remap->get_selected()->get_metadata(0);
508 }
509
510 translation_remap->clear();
511 translation_remap_options->clear();
512 root = translation_remap->create_item(nullptr);
513 TreeItem *root2 = translation_remap_options->create_item(nullptr);
514 translation_remap->set_hide_root(true);
515 translation_remap_options->set_hide_root(true);
516 translation_res_option_add_button->set_disabled(true);
517
518 if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps")) {
519 Dictionary remaps = GLOBAL_GET("internationalization/locale/translation_remaps");
520 List<Variant> rk;
521 remaps.get_key_list(&rk);
522 Vector<String> keys;
523 for (const Variant &E : rk) {
524 keys.push_back(E);
525 }
526 keys.sort();
527
528 for (int i = 0; i < keys.size(); i++) {
529 TreeItem *t = translation_remap->create_item(root);
530 t->set_editable(0, false);
531 t->set_text(0, keys[i].replace_first("res://", ""));
532 t->set_tooltip_text(0, keys[i]);
533 t->set_metadata(0, keys[i]);
534 t->add_button(0, get_editor_theme_icon(SNAME("Remove")), 0, false, TTR("Remove"));
535
536 // Display that it has been removed if this is the case.
537 if (!FileAccess::exists(keys[i])) {
538 t->set_text(0, t->get_text(0) + vformat(" (%s)", TTR("Removed")));
539 t->set_tooltip_text(0, vformat(TTR("%s cannot be found."), t->get_tooltip_text(0)));
540 }
541
542 if (keys[i] == remap_selected) {
543 t->select(0);
544 translation_res_option_add_button->set_disabled(false);
545
546 PackedStringArray selected = remaps[keys[i]];
547 for (int j = 0; j < selected.size(); j++) {
548 String s2 = selected[j];
549 int qp = s2.rfind(":");
550 String path = s2.substr(0, qp);
551 String locale = s2.substr(qp + 1, s2.length());
552
553 TreeItem *t2 = translation_remap_options->create_item(root2);
554 t2->set_editable(0, false);
555 t2->set_text(0, path.replace_first("res://", ""));
556 t2->set_tooltip_text(0, path);
557 t2->set_metadata(0, j);
558 t2->add_button(0, get_editor_theme_icon(SNAME("Remove")), 0, false, TTR("Remove"));
559 t2->set_cell_mode(1, TreeItem::CELL_MODE_CUSTOM);
560 t2->set_text(1, TranslationServer::get_singleton()->get_locale_name(locale));
561 t2->set_editable(1, true);
562 t2->set_metadata(1, path);
563 t2->set_tooltip_text(1, locale);
564
565 // Display that it has been removed if this is the case.
566 if (!FileAccess::exists(path)) {
567 t2->set_text(0, t2->get_text(0) + vformat(" (%s)", TTR("Removed")));
568 t2->set_tooltip_text(0, vformat(TTR("%s cannot be found."), t2->get_tooltip_text(0)));
569 }
570 }
571 }
572 }
573 }
574
575 // Update translation POT files.
576 translation_pot_list->clear();
577 root = translation_pot_list->create_item(nullptr);
578 translation_pot_list->set_hide_root(true);
579 PackedStringArray pot_translations = GLOBAL_GET("internationalization/locale/translations_pot_files");
580 for (int i = 0; i < pot_translations.size(); i++) {
581 TreeItem *t = translation_pot_list->create_item(root);
582 t->set_editable(0, false);
583 t->set_text(0, pot_translations[i].replace_first("res://", ""));
584 t->set_tooltip_text(0, pot_translations[i]);
585 t->set_metadata(0, i);
586 t->add_button(0, get_editor_theme_icon(SNAME("Remove")), 0, false, TTR("Remove"));
587 }
588
589 // New translation parser plugin might extend possible file extensions in POT generation.
590 _update_pot_file_extensions();
591
592 pot_generate_button->set_disabled(pot_translations.is_empty());
593
594 updating_translations = false;
595}
596
597void LocalizationEditor::_bind_methods() {
598 ClassDB::bind_method(D_METHOD("update_translations"), &LocalizationEditor::update_translations);
599
600 ADD_SIGNAL(MethodInfo("localization_changed"));
601}
602
603LocalizationEditor::LocalizationEditor() {
604 localization_changed = "localization_changed";
605
606 TabContainer *translations = memnew(TabContainer);
607 translations->set_v_size_flags(Control::SIZE_EXPAND_FILL);
608 add_child(translations);
609
610 {
611 VBoxContainer *tvb = memnew(VBoxContainer);
612 tvb->set_name(TTR("Translations"));
613 translations->add_child(tvb);
614
615 HBoxContainer *thb = memnew(HBoxContainer);
616 Label *l = memnew(Label(TTR("Translations:")));
617 l->set_theme_type_variation("HeaderSmall");
618 thb->add_child(l);
619 thb->add_spacer();
620 tvb->add_child(thb);
621
622 Button *addtr = memnew(Button(TTR("Add...")));
623 addtr->connect("pressed", callable_mp(this, &LocalizationEditor::_translation_file_open));
624 thb->add_child(addtr);
625
626 VBoxContainer *tmc = memnew(VBoxContainer);
627 tmc->set_v_size_flags(Control::SIZE_EXPAND_FILL);
628 tvb->add_child(tmc);
629
630 translation_list = memnew(Tree);
631 translation_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
632 tmc->add_child(translation_list);
633
634 locale_select = memnew(EditorLocaleDialog);
635 locale_select->connect("locale_selected", callable_mp(this, &LocalizationEditor::_translation_res_option_selected));
636 add_child(locale_select);
637
638 translation_file_open = memnew(EditorFileDialog);
639 translation_file_open->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILES);
640 translation_file_open->connect("files_selected", callable_mp(this, &LocalizationEditor::_translation_add));
641 add_child(translation_file_open);
642 }
643
644 {
645 VBoxContainer *tvb = memnew(VBoxContainer);
646 tvb->set_name(TTR("Remaps"));
647 translations->add_child(tvb);
648
649 HBoxContainer *thb = memnew(HBoxContainer);
650 Label *l = memnew(Label(TTR("Resources:")));
651 l->set_theme_type_variation("HeaderSmall");
652 thb->add_child(l);
653 thb->add_spacer();
654 tvb->add_child(thb);
655
656 Button *addtr = memnew(Button(TTR("Add...")));
657 addtr->connect("pressed", callable_mp(this, &LocalizationEditor::_translation_res_file_open));
658 thb->add_child(addtr);
659
660 VBoxContainer *tmc = memnew(VBoxContainer);
661 tmc->set_v_size_flags(Control::SIZE_EXPAND_FILL);
662 tvb->add_child(tmc);
663
664 translation_remap = memnew(Tree);
665 translation_remap->set_v_size_flags(Control::SIZE_EXPAND_FILL);
666 translation_remap->connect("cell_selected", callable_mp(this, &LocalizationEditor::_translation_res_select));
667 translation_remap->connect("button_clicked", callable_mp(this, &LocalizationEditor::_translation_res_delete));
668 tmc->add_child(translation_remap);
669
670 translation_res_file_open_dialog = memnew(EditorFileDialog);
671 translation_res_file_open_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILES);
672 translation_res_file_open_dialog->connect("files_selected", callable_mp(this, &LocalizationEditor::_translation_res_add));
673 add_child(translation_res_file_open_dialog);
674
675 thb = memnew(HBoxContainer);
676 l = memnew(Label(TTR("Remaps by Locale:")));
677 l->set_theme_type_variation("HeaderSmall");
678 thb->add_child(l);
679 thb->add_spacer();
680 tvb->add_child(thb);
681
682 addtr = memnew(Button(TTR("Add...")));
683 addtr->connect("pressed", callable_mp(this, &LocalizationEditor::_translation_res_option_file_open));
684 translation_res_option_add_button = addtr;
685 thb->add_child(addtr);
686
687 tmc = memnew(VBoxContainer);
688 tmc->set_v_size_flags(Control::SIZE_EXPAND_FILL);
689 tvb->add_child(tmc);
690
691 translation_remap_options = memnew(Tree);
692 translation_remap_options->set_v_size_flags(Control::SIZE_EXPAND_FILL);
693 translation_remap_options->set_columns(2);
694 translation_remap_options->set_column_title(0, TTR("Path"));
695 translation_remap_options->set_column_title(1, TTR("Locale"));
696 translation_remap_options->set_column_titles_visible(true);
697 translation_remap_options->set_column_expand(0, true);
698 translation_remap_options->set_column_clip_content(0, true);
699 translation_remap_options->set_column_expand(1, false);
700 translation_remap_options->set_column_clip_content(1, false);
701 translation_remap_options->set_column_custom_minimum_width(1, 250);
702 translation_remap_options->connect("item_edited", callable_mp(this, &LocalizationEditor::_translation_res_option_changed));
703 translation_remap_options->connect("button_clicked", callable_mp(this, &LocalizationEditor::_translation_res_option_delete));
704 translation_remap_options->connect("custom_popup_edited", callable_mp(this, &LocalizationEditor::_translation_res_option_popup));
705 tmc->add_child(translation_remap_options);
706
707 translation_res_option_file_open_dialog = memnew(EditorFileDialog);
708 translation_res_option_file_open_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILES);
709 translation_res_option_file_open_dialog->connect("files_selected", callable_mp(this, &LocalizationEditor::_translation_res_option_add));
710 add_child(translation_res_option_file_open_dialog);
711 }
712
713 {
714 VBoxContainer *tvb = memnew(VBoxContainer);
715 tvb->set_name(TTR("POT Generation"));
716 translations->add_child(tvb);
717
718 HBoxContainer *thb = memnew(HBoxContainer);
719 Label *l = memnew(Label(TTR("Files with translation strings:")));
720 l->set_theme_type_variation("HeaderSmall");
721 thb->add_child(l);
722 thb->add_spacer();
723 tvb->add_child(thb);
724
725 Button *addtr = memnew(Button(TTR("Add...")));
726 addtr->connect("pressed", callable_mp(this, &LocalizationEditor::_pot_file_open));
727 thb->add_child(addtr);
728
729 pot_generate_button = memnew(Button(TTR("Generate POT")));
730 pot_generate_button->connect("pressed", callable_mp(this, &LocalizationEditor::_pot_generate_open));
731 thb->add_child(pot_generate_button);
732
733 VBoxContainer *tmc = memnew(VBoxContainer);
734 tmc->set_v_size_flags(Control::SIZE_EXPAND_FILL);
735 tvb->add_child(tmc);
736
737 translation_pot_list = memnew(Tree);
738 translation_pot_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
739 tmc->add_child(translation_pot_list);
740
741 pot_generate_dialog = memnew(EditorFileDialog);
742 pot_generate_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
743 pot_generate_dialog->connect("file_selected", callable_mp(this, &LocalizationEditor::_pot_generate));
744 add_child(pot_generate_dialog);
745
746 pot_file_open_dialog = memnew(EditorFileDialog);
747 pot_file_open_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILES);
748 pot_file_open_dialog->connect("files_selected", callable_mp(this, &LocalizationEditor::_pot_add));
749 add_child(pot_file_open_dialog);
750 }
751}
752