Added strings extractor, fix dialogs not freed on plugin disable

This commit is contained in:
Marc Gilleron 2018-11-20 01:43:44 +00:00
parent f99df8d186
commit 126acb0247
6 changed files with 693 additions and 10 deletions

View File

@ -0,0 +1,193 @@
const STATE_SEARCHING = 0
const STATE_READING_TEXT = 1
signal finished(results)
var _strings = {}
var _thread = null
var _time_before = 0.0
var _ignored_paths = {}
func extract(root, ignored_paths=[]):
_time_before = OS.get_ticks_msec()
assert(_thread == null)
_ignored_paths.clear()
for p in ignored_paths:
_ignored_paths[root.plus_file(p)] = true
_thread = Thread.new()
_thread.start(self, "_extract", root)
func _extract(root):
_walk(root, funcref(self, "_process_file"), funcref(self, "_filter"))
call_deferred("_finished")
func _finished():
_thread.wait_to_finish()
_thread = null
var elapsed = float(OS.get_ticks_msec() - _time_before) / 1000.0
print("Extraction took ", elapsed, " seconds")
emit_signal("finished", _strings)
func _filter(path):
if path in _ignored_paths:
return false
return true
func _process_file(fpath):
var ext = fpath.get_extension()
#print("File ", fpath)
if ext != "tscn" and ext != "gd":
return
var f = File.new()
var err = f.open(fpath, File.READ)
if err != OK:
printerr("Could not open '", fpath, "', for read, error ", err)
return
match ext:
"tscn":
_process_tscn(f, fpath)
"gd":
_process_gd(f, fpath)
func _process_tscn(f, fpath):
var pattern = "text ="
var text = ""
var state = STATE_SEARCHING
var line_number = 0
while not f.eof_reached():
var line = f.get_line()
line_number += 1
if line == "":
continue
match state:
STATE_SEARCHING:
var i = line.find(pattern)
if i != -1:
var begin_quote_index = line.find('"', i + len(pattern))
if begin_quote_index == -1:
printerr("Could not find begin quote after text property, in ", fpath, " line ", line_number)
continue
var end_quote_index = line.rfind('"')
if end_quote_index != -1 and end_quote_index > begin_quote_index and line[end_quote_index - 1] != '\\':
text = line.substr(begin_quote_index + 1, end_quote_index - begin_quote_index - 1)
if text != "":
_add_string(fpath, line_number, text)
text = ""
else:
# The text may be multiline
text = str(line.right(begin_quote_index + 1), "\n")
state = STATE_READING_TEXT
STATE_READING_TEXT:
var end_quote_index = line.rfind('"')
if end_quote_index != -1 and line[end_quote_index - 1] != '\\':
text = str(text, line.left(end_quote_index))
_add_string(fpath, line_number, text)
text = ""
state = STATE_SEARCHING
else:
text = str(text, line, "\n")
func _process_gd(f, fpath):
var pattern = "tr("
var text = ""
var line_number = 0
while not f.eof_reached():
var line = f.get_line().strip_edges()
line_number += 1
if line == "" or line[0] == "#":
continue
# Search for one or multiple tr("...") in the same line
var search_index = 0
var counter = 0
while true:
var call_index = line.find(pattern, search_index)
if call_index == -1:
break
if call_index != 0:
if line.substr(call_index - 1, 3).is_valid_identifier():
# not a tr( call
break
if line[call_index - 1] == '"':
break
# TODO There may be more cases to handle
# They may need regexes or a simplified GDScript parser to extract properly
var begin_quote_index = line.find('"', call_index)
if begin_quote_index == -1:
# Multiline or procedural strings not supported
printerr("Begin quote not found in ", fpath, " line ", line_number)
break
var end_quote_index = find_unescaped_quote(line, begin_quote_index + 1)
if end_quote_index == -1:
# Multiline or procedural strings not supported
printerr("End quote not found in ", fpath, " line ", line_number)
break
text = line.substr(begin_quote_index + 1, end_quote_index - begin_quote_index - 1)
var end_bracket_index = line.find(')', end_quote_index)
if end_bracket_index == -1:
# Multiline or procedural strings not supported
printerr("End bracket not found in ", fpath, " line ", line_number)
break
_add_string(fpath, line_number, text)
search_index = end_bracket_index
counter += 1
assert(counter < 100)
static func find_unescaped_quote(s, from):
while true:
var i = s.find('"', from)
if i <= 0:
return i
if s[i - 1] != '\\':
return i
from = i + 1
func _add_string(file, line_number, text):
if not _strings.has(file):
_strings[file] = {}
_strings[file][text] = line_number
static func _walk(folder_path, file_action, filter):
#print("Walking dir ", folder_path)
var d = Directory.new()
var err = d.open(folder_path)
if err != OK:
printerr("Could not open directory '", folder_path, "', error ", err)
return
d.list_dir_begin(true, true)
var fname = d.get_next()
while fname != "":
var fullpath = folder_path.plus_file(fname)
if filter == null or filter.call_func(fullpath) == true:
if d.current_is_dir():
_walk(fullpath, file_action, filter)
else:
file_action.call_func(fullpath)
fname = d.get_next()
return

View File

@ -0,0 +1,127 @@
tool
extends WindowDialog
const Extractor = preload("extractor.gd")
signal import_selected(strings)
onready var _root_path_edit = get_node("VBoxContainer/HBoxContainer/RootPathEdit")
onready var _summary_label = get_node("VBoxContainer/SummaryLabel")
onready var _results_list = get_node("VBoxContainer/Results")
onready var _progress_bar = get_node("VBoxContainer/ProgressBar")
onready var _extract_button = get_node("VBoxContainer/Buttons/ExtractButton")
onready var _import_button = get_node("VBoxContainer/Buttons/ImportButton")
var _extractor = null
var _results = {}
var _registered_string_filter = null
func _ready():
_import_button.disabled = true
func set_registered_string_filter(registered_string_filter):
assert(registered_string_filter is FuncRef)
_registered_string_filter = registered_string_filter
func _notification(what):
if what == NOTIFICATION_VISIBILITY_CHANGED:
if visible:
_summary_label.text = ""
_results.clear()
_update_import_button()
func _update_import_button():
_import_button.disabled = (_results == null or len(_results) == 0)
func _on_ExtractButton_pressed():
if _extractor != null:
return
var root = _root_path_edit.text.strip_edges()
var d = Directory.new()
if not d.dir_exists(root):
printerr("Directory `", root, "` does not exist")
return
_extractor = Extractor.new()
_extractor.connect("finished", self, "_on_Extractor_finished")
#_extractor.extract("res://", ["addons"])
_extractor.extract("res://", [])
# TODO Progress reporting
_progress_bar.value = 50
_extract_button.disabled = true
_import_button.disabled = true
_summary_label.text = "Extracting..."
func _on_ImportButton_pressed():
emit_signal("import_selected", _results)
_results.clear()
hide()
func _on_CancelButton_pressed():
# TODO Cancel extraction?
hide()
func _on_Extractor_finished(results):
print("Extractor finished")
_progress_bar.value = 100
_results_list.clear()
var registered_set = {}
var new_set = {}
# TODO We might actually want to not filter, in order to update location comments
# Filter results
if _registered_string_filter != null:
var fpaths = results.keys()
for fpath in fpaths:
var strings_dict = results[fpath]
var strings = strings_dict.keys()
for text in strings:
if _registered_string_filter.call_func(text):
strings_dict.erase(text)
registered_set[text] = true
if len(strings_dict) == 0:
results.erase(fpath)
# Root
_results_list.create_item()
for fpath in results:
#print(fpath)
var strings = results[fpath]
for text in strings:
var line_number = strings[text]
#print(" ", line_number, ": `", text, "`")
var item = _results_list.create_item()
item.set_text(0, text)
item.set_text(1, str(fpath, ": ", line_number))
#item.set_tooltip(
item.set_metadata(1, fpath)
new_set[text] = true
_results = results
_extractor = null
_update_import_button()
_extract_button.disabled = false
_summary_label.text = "{0} new, {1} registered".format([len(new_set), len(registered_set)])

View File

@ -0,0 +1,311 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://addons/zylann.translation_editor/tools/extractor_dialog.gd" type="Script" id=1]
[node name="ExtractorDialog" type="WindowDialog"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 54.0
margin_top = 68.0
margin_right = 694.0
margin_bottom = 548.0
rect_min_size = Vector2( 640, 480 )
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
popup_exclusive = false
window_title = "String extractor"
resizable = true
script = ExtResource( 1 )
_sections_unfolded = [ "Rect" ]
[node name="VBoxContainer" type="VBoxContainer" parent="." index="1"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 8.0
margin_top = 8.0
margin_right = -8.0
margin_bottom = -8.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 1
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
alignment = 0
_sections_unfolded = [ "Margin" ]
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" index="0"]
editor/display_folded = true
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 624.0
margin_bottom = 24.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 1
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
alignment = 0
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 5.0
margin_right = 29.0
margin_bottom = 19.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 4
text = "Root"
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
[node name="RootPathEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer" index="1"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 33.0
margin_right = 624.0
margin_bottom = 24.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 1
size_flags_horizontal = 3
size_flags_vertical = 1
text = "res://"
focus_mode = 2
context_menu_enabled = true
placeholder_alpha = 0.6
caret_blink = false
caret_blink_speed = 0.65
caret_position = 0
_sections_unfolded = [ "Size Flags" ]
[node name="SummaryLabel" type="Label" parent="VBoxContainer" index="1"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 28.0
margin_right = 624.0
margin_bottom = 42.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 4
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
[node name="Results" type="Tree" parent="VBoxContainer" index="2"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 46.0
margin_right = 624.0
margin_bottom = 396.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = true
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 3
columns = 2
allow_reselect = false
allow_rmb_select = false
hide_folding = false
hide_root = true
drop_mode_flags = 0
select_mode = 1
_sections_unfolded = [ "Size Flags" ]
[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer" index="3"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 400.0
margin_right = 624.0
margin_bottom = 416.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 0
min_value = 0.0
max_value = 100.0
step = 1.0
page = 0.0
value = 0.0
exp_edit = false
rounded = false
percent_visible = true
[node name="Spacer" type="Control" parent="VBoxContainer" index="4"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 420.0
margin_right = 624.0
margin_bottom = 428.0
rect_min_size = Vector2( 0, 8 )
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
_sections_unfolded = [ "Rect" ]
[node name="Buttons" type="HBoxContainer" parent="VBoxContainer" index="5"]
editor/display_folded = true
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 432.0
margin_right = 624.0
margin_bottom = 452.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 1
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
custom_constants/separation = 16
alignment = 1
_sections_unfolded = [ "custom_constants" ]
[node name="ExtractButton" type="Button" parent="VBoxContainer/Buttons" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 184.0
margin_right = 239.0
margin_bottom = 20.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = "Extract"
flat = false
align = 1
[node name="ImportButton" type="Button" parent="VBoxContainer/Buttons" index="1"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 255.0
margin_right = 370.0
margin_bottom = 20.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = "Import selected"
flat = false
align = 1
[node name="CancelButton" type="Button" parent="VBoxContainer/Buttons" index="2"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 386.0
margin_right = 440.0
margin_bottom = 20.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = "Cancel"
flat = false
align = 1
[node name="Spacer2" type="Control" parent="VBoxContainer" index="6"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 456.0
margin_right = 624.0
margin_bottom = 464.0
rect_min_size = Vector2( 0, 8 )
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
_sections_unfolded = [ "Rect" ]
[connection signal="pressed" from="VBoxContainer/Buttons/ExtractButton" to="." method="_on_ExtractButton_pressed"]
[connection signal="pressed" from="VBoxContainer/Buttons/ImportButton" to="." method="_on_ImportButton_pressed"]
[connection signal="pressed" from="VBoxContainer/Buttons/CancelButton" to="." method="_on_CancelButton_pressed"]

View File

@ -19,6 +19,7 @@ func _enter_tree():
func _exit_tree():
print("Translation editor plugin Exit tree")
# The main control is not freed when the plugin is disabled
_main_control.queue_free()
_main_control = null

View File

@ -2,9 +2,8 @@
[ext_resource path="res://addons/zylann.translation_editor/tools/string_edition_dialog.gd" type="Script" id=1]
[node name="StringEditionDialog" type="WindowDialog"]
[node name="StringEditionDialog" type="WindowDialog" index="0"]
visible = false
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
@ -20,7 +19,7 @@ mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
popup_exclusive = false
window_title = "New string"
window_title = "New string ID"
resizable = false
script = ExtResource( 1 )
@ -57,7 +56,6 @@ mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 4
text = "Already existing"
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
@ -139,6 +137,7 @@ mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
disabled = true
toggle_mode = false
enabled_focus_mode = 2
shortcut = null

View File

@ -6,6 +6,7 @@ const PoLoader = preload("po_loader.gd")
const Locales = preload("locales.gd")
const StringEditionDialog = preload("string_edition_dialog.tscn")
const LanguageSelectionDialog = preload("language_selection_dialog.tscn")
const ExtractorDialog = preload("extractor_dialog.tscn")
const MENU_FILE_OPEN = 0
const MENU_FILE_SAVE = 1
@ -13,6 +14,7 @@ const MENU_FILE_SAVE_AS_CSV = 2
const MENU_FILE_SAVE_AS_PO = 3
const MENU_FILE_ADD_LANGUAGE = 4
const MENU_FILE_REMOVE_LANGUAGE = 5
const MENU_FILE_EXTRACT = 6
const FORMAT_CSV = 0
const FORMAT_GETTEXT = 1
@ -28,12 +30,14 @@ onready var _status_label = get_node("VBoxContainer/StatusBar/Label")
var _string_edit_dialog = null
var _language_selection_dialog = null
var _remove_language_confirmation_dialog = null
var _extractor_dialog = null
var _open_dialog = null
var _save_file_dialog = null
var _save_folder_dialog = null
# This is set when integrated as a Godot plugin
var _base_control = null
var _translation_edits = {}
var _dialogs_to_free_on_exit = []
var _data = {}
var _languages = []
@ -54,6 +58,8 @@ func _ready():
_file_menu.get_popup().add_separator()
_file_menu.get_popup().add_item("Add language...", MENU_FILE_ADD_LANGUAGE)
_file_menu.get_popup().add_item("Remove language", MENU_FILE_REMOVE_LANGUAGE)
_file_menu.get_popup().add_separator()
_file_menu.get_popup().add_item("Extractor", MENU_FILE_EXTRACT)
_file_menu.get_popup().set_item_disabled(_file_menu.get_popup().get_item_index(MENU_FILE_REMOVE_LANGUAGE), true)
_file_menu.get_popup().connect("id_pressed", self, "_on_FileMenu_id_pressed")
@ -70,40 +76,62 @@ func _ready():
func _setup_dialogs(dialogs_parent):
# If this fails, something wrong is happening with parenting of the main view
assert(_open_dialog == null)
_open_dialog = FileDialog.new()
_open_dialog.window_title = "Open translations"
_open_dialog.add_filter("*.csv ; CSV files")
_open_dialog.add_filter("*.po ; Gettext files")
_open_dialog.mode = FileDialog.MODE_OPEN_FILE
_open_dialog.connect("file_selected", self, "_on_OpenDialog_file_selected")
dialogs_parent.add_child(_open_dialog)
_add_dialog(dialogs_parent, _open_dialog)
_save_file_dialog = FileDialog.new()
_save_file_dialog.window_title = "Save translations as CSV"
_save_file_dialog.add_filter("*.csv ; CSV files")
_save_file_dialog.mode = FileDialog.MODE_SAVE_FILE
_save_file_dialog.connect("file_selected", self, "_on_SaveFileDialog_file_selected")
dialogs_parent.add_child(_save_file_dialog)
_add_dialog(dialogs_parent, _save_file_dialog)
_save_folder_dialog = FileDialog.new()
_save_folder_dialog.window_title = "Save translations as gettext .po files"
_save_folder_dialog.mode = FileDialog.MODE_OPEN_DIR
_save_folder_dialog.connect("dir_selected", self, "_on_SaveFolderDialog_dir_selected")
dialogs_parent.add_child(_save_folder_dialog)
_add_dialog(dialogs_parent, _save_folder_dialog)
_string_edit_dialog = StringEditionDialog.instance()
_string_edit_dialog.set_validator(funcref(self, "_validate_new_string_id"))
_string_edit_dialog.connect("submitted", self, "_on_StringEditionDialog_submitted")
dialogs_parent.add_child(_string_edit_dialog)
_add_dialog(dialogs_parent, _string_edit_dialog)
_language_selection_dialog = LanguageSelectionDialog.instance()
_language_selection_dialog.connect("language_selected", self, "_on_LanguageSelectionDialog_language_selected")
dialogs_parent.add_child(_language_selection_dialog)
_add_dialog(dialogs_parent, _language_selection_dialog)
_remove_language_confirmation_dialog = ConfirmationDialog.new()
_remove_language_confirmation_dialog.dialog_text = "Do you really want to remove this language? (There is no undo!)"
_remove_language_confirmation_dialog.connect("confirmed", self, "_on_RemoveLanguageConfirmationDialog_confirmed")
dialogs_parent.add_child(_remove_language_confirmation_dialog)
_add_dialog(dialogs_parent, _remove_language_confirmation_dialog)
_extractor_dialog = ExtractorDialog.instance()
_extractor_dialog.set_registered_string_filter(funcref(self, "_is_string_registered"))
_extractor_dialog.connect("import_selected", self, "_on_ExtractorDialog_import_selected")
_add_dialog(dialogs_parent, _extractor_dialog)
func _add_dialog(parent, dialog):
parent.add_child(dialog)
if parent != self:
_dialogs_to_free_on_exit.append(dialog)
func _exit_tree():
# Free dialogs because in the editor they might not be child of the main view...
# Also this code runs in the edited scene view as a `tool` side-effect.
for dialog in _dialogs_to_free_on_exit:
dialog.queue_free()
_dialogs_to_free_on_exit.clear()
func configure_for_godot_integration(base_control):
@ -137,6 +165,9 @@ func _on_FileMenu_id_pressed(id):
var language = get_current_language()
_remove_language_confirmation_dialog.window_title = str("Remove language `", language, "`")
_remove_language_confirmation_dialog.popup_centered_minsize()
MENU_FILE_EXTRACT:
_extractor_dialog.popup_centered_minsize()
func _on_EditMenu_id_pressed(id):
@ -400,6 +431,8 @@ func add_new_string(strid):
}
_data[strid] = s
_string_list.add_item(strid)
for language in _languages:
_set_language_modified(language)
func rename_string(old_strid, new_strid):
@ -445,3 +478,22 @@ func _remove_language(language):
func _on_RemoveLanguageConfirmationDialog_confirmed():
var language = get_current_language()
_remove_language(language)
# Currently used as callback for filtering
func _is_string_registered(text):
if _data == null:
print("No data")
return false
return _data.has(text)
func _on_ExtractorDialog_import_selected(results):
for fpath in results:
var strings = results[fpath]
for text in strings:
# Checking because there might be duplicates,
# strings can be found in multiple places
if not _is_string_registered(text):
add_new_string(text)