1#!/usr/bin/python3
2
3from gi.repository import Gtk
4import tokenize
5import token
6import re
7
8def match_ignore(string):
9	if string.startswith("'''"):
10		return True # ignore docstring etc.
11
12	string = string.strip('\'"')
13	if not re.search(r'[a-zA-Z]', string):
14		return True # ignore without any letter
15	elif len(string) < 3:
16		return True # ignore 'w' etc.
17	elif re.match(r'^</?\w+>$', string):
18		return True # ignore open / close XML tags
19	elif string.startswith('zim.'):
20		return True # module names
21	elif string.startswith('BUG'):
22		return True # assertion (?)
23	elif string.startswith('TODO'):
24		return True # assertion (?)
25
26	return False
27
28ignore_functions = ('setdefault', 'connect', 'connect_after', 'connect_object', 'get_property', 'emit', 'info', 'debug', 'warn', 'exception', 'get_action')
29
30
31class Internationalizer(Gtk.Window):
32
33	def __init__(self):
34	#~ def __init__(self, dir):
35		GObject.GObject.__init__(self)
36		self.set_title('Internationalizer')
37		self.set_default_size(500, 500)
38		#~ self.dir = dir
39
40		vbox = Gtk.VBox()
41		self.add(vbox)
42
43		self.status_label = Gtk.Label()
44		vbox.pack_start(self.status_label, False)
45
46		hbox = Gtk.HBox()
47		vbox.add(hbox)
48		scrollwindow = Gtk.ScrolledWindow()
49		hbox.add(scrollwindow)
50
51		self.textview = Gtk.TextView()
52		self.textview.set_left_margin(12)
53		self.textview.set_right_margin(5)
54		scrollwindow.add(self.textview)
55
56		bbox = Gtk.HButtonBox()
57		vbox.pack_start(bbox, False)
58
59		savebutton = Gtk.Button(stock='gtk-save')
60		savebutton.connect_object('clicked', self.__class__.save_file, self)
61		bbox.add(savebutton)
62
63		reloadbutton = Gtk.Button(stock='gtk-refresh')
64		reloadbutton.connect_object('clicked', self.__class__.reload_file, self)
65		bbox.add(reloadbutton)
66
67		nextbutton = Gtk.Button(stock='gtk-forward')
68		nextbutton.connect_object('clicked', self.__class__.next_tag, self)
69		bbox.add(nextbutton)
70
71		applybutton = Gtk.Button(stock='gtk-apply')
72		applybutton.connect_object('clicked', self.__class__.apply_mark, self)
73		bbox.add(applybutton)
74
75
76	def open_file(self, file):
77		if self.textview.get_buffer().get_modified():
78			self.save_file()
79		self.file = file
80		buffer = Gtk.TextBuffer()
81		print('Reading %s' % self.file)
82		buffer.set_text(open(self.file).read())
83		self.textview.set_buffer(buffer)
84
85		buffer.create_tag('translated', background='green')
86		buffer.create_tag('untranslated', background='red')
87		buffer.create_tag('notsure', background='orange')
88
89		translated, untranslated, notsure = self.tokenize()
90		self.status_label.set_text(
91			"%i translated, %i untranslated, %i not sure"
92			% (len(translated), len(untranslated), len(notsure))
93		)
94
95		def get_iter(coord):
96			row, col = coord
97			row -= 1
98			iter = buffer.get_iter_at_line(row)
99			iter.forward_chars(col)
100			return iter
101
102		for start, end in translated:
103			start, end = list(map(get_iter, (start, end)))
104			buffer.apply_tag_by_name('translated', start, end)
105
106		for start, end in untranslated:
107			start, end = list(map(get_iter, (start, end)))
108			buffer.apply_tag_by_name('untranslated', start, end)
109
110		for start, end in notsure:
111			start, end = list(map(get_iter, (start, end)))
112			buffer.apply_tag_by_name('notsure', start, end)
113
114		buffer.place_cursor(buffer.get_iter_at_offset(0))
115
116	def tokenize(self):
117		translated = []
118		untranslated = []
119		notsure = []
120		tokens = tokenize.generate_tokens(open(self.file).readline)
121
122		reset = lambda: {'funcname': None, 'isfunc': False, 'iskey': False}
123		state = reset()
124
125		for type, string, start, end, line in tokens:
126			if type == token.STRING:
127				if state['isfunc'] and state['funcname'] == '_':
128					translated.append((start, end))
129				elif state['iskey'] or \
130				(state['isfunc'] and state['funcname'] in ignore_functions) or \
131				match_ignore(string):
132					notsure.append((start, end))
133				else:
134					untranslated.append((start, end))
135				state = reset()
136			elif type == token.NAME:
137				state = reset()
138				state['funcname'] = string
139			elif type == token.OP and string == '(':
140				state['iskey'] = False
141				state['isfunc'] = True
142			elif type == token.OP and string == '[':
143				state['isfunc'] = False
144				state['iskey'] = True
145			else:
146				state = reset()
147
148		return translated, untranslated, notsure
149
150	def save_file(self):
151		buffer = self.textview.get_buffer()
152		content = buffer.get_text(*buffer.get_bounds())
153		print('Writing %s' % self.file)
154		open(self.file, 'w').write(content)
155
156	def reload_file(self):
157		self.open_file(self.file)
158
159	def next_tag(self):
160		'''Select the next untranslated string'''
161		buffer = self.textview.get_buffer()
162		iter = buffer.get_iter_at_mark(buffer.get_insert())
163		tag = buffer.get_tag_table().lookup('untranslated')
164		iter.forward_to_tag_toggle(tag)
165		if not iter.begins_tag(tag):
166			iter.forward_to_tag_toggle(tag)
167		bound = iter.copy()
168		bound.forward_to_tag_toggle(tag)
169		buffer.select_range(iter, bound)
170		self.textview.scroll_mark_onscreen(buffer.get_selection_bound())
171		self.textview.scroll_mark_onscreen(buffer.get_insert())
172
173	def apply_mark(self):
174		'''Wrap current selected string in "_( .. )"'''
175		buffer = self.textview.get_buffer()
176		bounds = buffer.get_selection_bounds()
177		if bounds:
178			start, end = bounds
179		else:
180			iter = buffer.get_iter_at_mark(buffer.get_insert())
181			for tag in 'untranslated', 'notsure':
182				tag = buffer.get_tag_table().lookup(tag)
183				if iter.has_tag(tag):
184					iter.backward_to_tag_toggle(tag)
185					bound = iter.copy()
186					bound.forward_to_tag_toggle(tag)
187					start, end = iter, bound
188					break
189			else:
190				return
191
192		buffer.remove_all_tags(start, end)
193		buffer.apply_tag_by_name('translated', start, end)
194		start, end = start.get_offset(), end.get_offset()
195		if start > end:
196			start, end = end, start
197		buffer.insert(buffer.get_iter_at_offset(end), ')')
198		buffer.insert(buffer.get_iter_at_offset(start), '_(')
199		self.next_tag()
200
201if __name__ == '__main__':
202	import sys
203	app = Internationalizer()
204	app.open_file(sys.argv[1])
205	app.show_all()
206	app.connect('destroy', lambda o: Gtk.main_quit())
207	Gtk.main()
208