dogtail-devel Partially-working automatic i18n support



I did some more work on dealing with translations in script files, and
have got automatic translation working... well, almost :-)

Attached is a reworking of my last i18n test code into a proposed
dogtail/i18n.py module, together with a patch to various files, and a
new test script.  

The idea is that you can install a translation database of some kind.
One type of translation database is the approach demonstrated in an
earlier post, involving scraping the package database to find gettext mo
files to determine the gettext translation domains of interest. So I
wrote a subclass that implements this.  This is a greybox testing
approach (assuming I understand the term).  Other subclasses could be
implemented, either leveraging other implementations of i18n (for 3rd-
party apps not using GNU gettext), or supplying their own translations
in the test scripts, for more of a blackbox approach.

This translation database is a singleton/global throughout the lifetime
of the script, and is used to pretranslate the strings in each search
predicate, so that you're doing string matching against translated
strings (and it means you only run the gettext translation lookup once
per search, rather than per-search-per-node-visited, so it ought to be
fairly efficient; my first pass at this used the latter approach and was
noticeably slower running scripts, with lots of CPU churn).

I patched the evolution wrapper so that it automatically installs the
"scraped from the package database" translation db, and took an existing
script (evolution-test-configuring-imap-smtp) and tried to get it to
work otherwise unchanged with evolution and the script running in
LANG=fr_FR.UTF-8

It gets some way through the script and then runs into problems; here's
the debug log (on an FC3 box): 

Detecting distribution:  Red Hat/Fedora/derived distribution
Warning: Dogtail could not import the Python bindings for libwnck.  Window-manager manipulation will not be available.
Evolution version 2.0.4
evolution-data-server version 1.0.4
gtkhtml3 version 3.3.2
click on {"Settings..." ("Param�es...") menuitem}

(evolution:24185): GLib-GObject-WARNING **: g_object_weak_unref: couldn't find weak ref 0x6013d6(0xb7bf4cf4)

(evolution:24185): Gtk-CRITICAL **: file gtktreesortable.c: line 137 (gtk_tree_sortable_set_sort_column_id): assertion `GTK_IS_TREE_SORTABLE (sortable)' failed

** (evolution:24185): CRITICAL **: file utils.c: line 100 (html_utils_get_accessible): assertion `o != NULL' failed

(evolution:24185): Gtk-CRITICAL **: file gtktogglebutton.c: line 326 (gtk_toggle_button_get_active): assertion `GTK_IS_TOGGLE_BUTTON (toggle_button)' failed
searching for descendent of {child with name="Evolution Settings" ("Param�es d'Evolution")}: child with name="Add" ("Add") (attempt 3)
searching for descendent of {child with name="Evolution Settings" ("Param�es d'Evolution")}: child with name="Add" ("Add") (attempt 4)
searching for descendent of {child with name="Evolution Settings" ("Param�es d'Evolution")}: child with name="Add" ("Add") (attempt 5)
searching for descendent of {child with name="Evolution Settings" ("Param�es d'Evolution")}: child with name="Add" ("Add") (attempt 6)
searching for descendent of {child with name="Evolution Settings" ("Param�es d'Evolution")}: child with name="Add" ("Add") (attempt 7)
searching for descendent of {child with name="Evolution Settings" ("Param�es d'Evolution")}: child with name="Add" ("Add") (attempt 8)
searching for descendent of {child with name="Evolution Settings" ("Param�es d'Evolution")}: child with name="Add" ("Add") (attempt 9)

etc...

It successfully translates the clicks on Tools->Settings and the
settings dialog name into the french equivalents, but runs into problems
clicking on the Add button for adding an account; "Add" isn't found in
any of the mo files suggested by the RPM database.  This string comes
from a stock item: "gtk-add", referenced in a glade file 
(mail-config.glade)  I'm not sure ATM how to deal with this
(specialcasing? a bug? an oversight?)

(I also patched the predicate classes so that the search and node
descriptions in the log file contain both the script string, and the
translated string, if available; you can see this in the logs above).

I also added a getUserVisibleStrings method to Node, and the i18-test.py
script mutated somewhat into something that scrapes all strings out of
an app and tries to autotranslate them (I've attached that script);
here's the result of running the script on evolution running in English
and translating to French:

Detecting distribution:  Red Hat/Fedora/derived distribution
Warning: Dogtail could not import the Python bindings for libwnck.  Window-manager manipulation will not be available.
User-visible string: evolution
Translation in fr locale is:evolution
User-visible string: Evolution - Mail
Translation in fr locale is:Evolution - Mail
User-visible string: grip
Translation in fr locale is:grip
User-visible string: File
Translation in fr locale is:Fichier
User-visible string: New
Translation in fr locale is:Nouveau
User-visible string: Mail Message
Translation in fr locale is:Mail Message
User-visible string: Mail Folder
Translation in fr locale is:Mail Folder
User-visible string: All Day Appointment
Translation in fr locale is:All Day Appointment
User-visible string: Appointment
Translation in fr locale is:Rendez-vous
User-visible string: Assigned Task
Translation in fr locale is:Assigned Task
User-visible string: Contact
Translation in fr locale is:Contact
User-visible string: Contact List
Translation in fr locale is:Contact List
User-visible string: Meeting
Translation in fr locale is:R�ion
User-visible string: Task
Translation in fr locale is:T�e
User-visible string: Address Book
Translation in fr locale is:Carnet d'adresses
User-visible string: Calendar
Translation in fr locale is:Calendrier
User-visible string: Task list
Translation in fr locale is:Task list
User-visible string: New Window
Translation in fr locale is:New Window
User-visible string: Open Message
Translation in fr locale is:Open Message
User-visible string: Save As...
Translation in fr locale is:Enregistrer sous...
User-visible string: Import...
Translation in fr locale is:Importer...
User-visible string: Print Preview
Translation in fr locale is:Aper�avant impression
User-visible string: Print...
Translation in fr locale is:Print...
User-visible string: Folder
Translation in fr locale is:Dossier
User-visible string: Properties
Translation in fr locale is:Propri�s
User-visible string: Work Offline
Translation in fr locale is:Travail hors ligne
User-visible string: Close
Translation in fr locale is:Fermer

etc...

Thoughts?

The patch to predicate.py probably breaks examples/recorder.py somewhat,
but that script was fairly broken anyway

Plus I had to hack away parts of logger to get things working on FC3;
the trayicon stuff uses "subprocess", which is a Python 2.4 thing, and
FC3 only has Python 2.3

Also, I don't how much this affects procedural.py; but it's getting late
and I really should sleep now...


Dave


Attachment: i18n.py
Description: application/python

Index: dogtail/distro.py
===================================================================
RCS file: /cvs/gnome/dogtail/dogtail/distro.py,v
retrieving revision 1.4
diff -u -p -r1.4 distro.py
--- dogtail/distro.py	7 Oct 2005 23:41:09 -0000	1.4
+++ dogtail/distro.py	21 Oct 2005 05:23:22 -0000
@@ -18,7 +18,22 @@ class PackageDb:
 		Method to get the version of an installed package as a Version instance (or raise an exception if not found)
 		Note: does not know about distributions' internal revision numbers.
 		"""
-		raise NotImplemented
+		raise NotImplementedError
+
+	def getFiles(self, packageName):
+		"""
+		Method to get a list of filenames owned by the package, or 
+		raise an exception if not found.
+		"""
+		raise NotImplementedError
+
+	def getDependencies(self, packageName):
+		"""
+		Method to get a list of unique package names that this package 
+		is dependent on, or raise an exception if the package is not 
+		found.
+		"""
+		raise NotImplementedError
 
 class Distro:
 	"""
@@ -56,7 +71,9 @@ class Conary(Distro):
 
 def __makeRpmPackageDb():
 	"""
-	Manufacture a PackageDb for an RPM system.  We hide this inside a factory method so that we only import the RPM Python bindings if we're on a platform likely to have them
+	Manufacture a PackageDb for an RPM system.  We hide this inside a 
+	factory method so that we only import the RPM Python bindings if we're
+	on a platform likely to have them
 	"""
 	class RpmPackageDb(PackageDb):
 		def getVersion(self, packageName):
@@ -65,9 +82,40 @@ def __makeRpmPackageDb():
 			for header in ts.dbMatch("name", packageName):
 				return Version.fromString(header["version"])
 			raise "Package not found: %s"%packageName
+
+		def getFiles(self, packageName):
+			import rpm
+			ts = rpm.TransactionSet()
+			for header in ts.dbMatch("name", packageName):
+				return header["filenames"]
+			raise "Package not found: %s"%packageName
+	
+		def getDependencies(self, packageName):
+			import rpm
+			ts = rpm.TransactionSet()
+			for header in ts.dbMatch("name", packageName):
+				# Simulate a set using a hash (to a dummy value);
+				# sets were only added in Python 2.4
+				result = {}
+
+				# Get the list of requirements; these are 
+				# sometimes package names, but can also be
+				# so-names of libraries, and invented virtual 
+				# ids
+				for requirement in header[rpm.RPMTAG_REQUIRES]:
+					# Get the name of the package providing
+					# this requirement:
+					for depPackageHeader in ts.dbMatch("provides", requirement):
+						depName = depPackageHeader['name']
+						if depName!=packageName:
+							# Add to the Hash with a dummy value
+							result[depName]=None
+				return result.keys()
+			raise "Package not found: %s"%packageName
+
 	return RpmPackageDb()
 
-PATCH_MESSAGE = "Please send patches to the mailing list." # FIXME: add mailing list address
+PATCH_MESSAGE = "Please send patches to dogtail-devel-list gnome org"
 
 def __makeAptPackageDb():
 	"""
Index: dogtail/predicate.py
===================================================================
RCS file: /cvs/gnome/dogtail/dogtail/predicate.py,v
retrieving revision 1.2
diff -u -p -r1.2 predicate.py
--- dogtail/predicate.py	17 Oct 2005 23:51:00 -0000	1.2
+++ dogtail/predicate.py	21 Oct 2005 05:23:22 -0000
@@ -5,6 +5,14 @@ __author__ = 'David Malcolm <dmalcolm re
 
 import unittest
 
+import dogtail.i18n
+from dogtail.i18n import TranslatableString
+
+def stringMatches(scriptName, reportedName):
+	assert isinstance(scriptName, TranslatableString)
+	
+	return scriptName.matchedBy(reportedName)
+	
 def makeScriptRecursiveArgument(isRecursive, defaultValue):
 	if isRecursive==defaultValue:
 		return ""
@@ -77,16 +85,17 @@ class Predicate:
 		else:
 			return self.__dict__ == other.__dict__
 
+
 class IsAnApplicationNamed(Predicate):
 	"""Search subclass that looks for an application by name"""
 	def __init__(self, appName):
-		self.appName = appName
+		self.appName = TranslatableString(appName)
 
 	def satisfiedByNode(self, node):
-		return node.roleName=='application' and node.name==self.appName
+		return node.roleName=='application' and stringMatches(self.appName, node.name)
 
 	def describeSearchResult(self):
-		return '"%s" application'%self.appName
+		return '%s application'%self.appName
 
 	def makeScriptMethodCall(self, isRecursive):
 		# ignores the isRecursive parameter
@@ -99,7 +108,7 @@ class GenericPredicate(Predicate):
 	"""SubtreePredicate subclass that takes various optional search fields"""
 
 	def __init__(self, name = '', roleName = '', description= '', label = '', debugName=None):
-		self.name = name
+		self.name = TranslatableString(name)
 		self.roleName = roleName
 		self.description = description
 		self.label = label
@@ -112,7 +121,7 @@ class GenericPredicate(Predicate):
 			else:
 				self.debugName = "child with"
 			if name:
-				self.debugName += " name='%s'"%name
+				self.debugName += " name=%s"%self.name	
 			if roleName:
 				self.debugName += " roleName='%s'"%roleName
 			if description:
@@ -132,7 +141,7 @@ class GenericPredicate(Predicate):
 		else:
 			# Ensure the node matches any criteria that were set:
 			if self.name:
-				if self.name!=node.name: return False
+				if not stringMatches(self.name,node.name): return False
 			if self.roleName:
 				if self.roleName!=node.roleName: return False
 			if self.description:
@@ -170,13 +179,13 @@ class IsNamed(Predicate):
 	"""Predicate subclass that looks simply by name"""
 
 	def __init__(self, name):
-		self.name = name
+		self.name = TranslatableString(name)
 	
 	def satisfiedByNode(self, node):
-		return node.name==self.name
+		return stringMatches(self.name, node.name)
 
 	def describeSearchResult(self):
-		return "named '%s'"%self.name
+		return "named %s"%self.name
 
 	def makeScriptMethodCall(self, isRecursive):
 		return "child(name='%s'%s)"%(self.name, makeScriptRecursiveArgument(isRecursive, True))
@@ -186,13 +195,13 @@ class IsNamed(Predicate):
 class IsAWindowNamed(Predicate):
 	"""Predicate subclass that looks for a top-level window by name"""
 	def __init__(self, windowName):
-		self.windowName = windowName
+		self.windowName = TranslatableString(windowName)
 
 	def satisfiedByNode(self, node):
-		return node.roleName=='frame' and node.name==self.windowName
+		return node.roleName=='frame' and stringMatches(self.windowName, node.name)
 
 	def describeSearchResult(self):
-		return "'%s' window"%self.windowName
+		return "%s window"%self.windowName
 
 	def makeScriptMethodCall(self, isRecursive):
 		return "window('%s'%s)"%(self.windowName, makeScriptRecursiveArgument(isRecursive, False))
@@ -211,13 +220,13 @@ class IsAWindow(Predicate):
 class IsADialogNamed(Predicate):
 	"""Predicate subclass that looks for a top-level dialog by name"""
 	def __init__(self, dialogName):
-		self.dialogName = dialogName
+		self.dialogName = TranslatableString(dialogName)
 
 	def satisfiedByNode(self, node):
-		return node.roleName=='dialog' and node.name==self.dialogName
+		return node.roleName=='dialog' and stringMatches(self.dialogName, node.name)
 
 	def describeSearchResult(self):
-		return '"%s" dialog'%self.dialogName
+		return '%s dialog'%self.dialogName
 
 	def makeScriptMethodCall(self, isRecursive):
 		return "dialog('%s'%s)"%(self.dialogName, makeScriptRecursiveArgument(isRecursive, False))
@@ -232,16 +241,16 @@ class IsLabelledBy(Predicate):
 class IsLabelledAs(Predicate):
 	"""Predicate: is this node labelled with the text string (i.e. by another node with that as a name)"""
 	def __init__(self, labelText):
-		self.labelText = labelText
+		self.labelText = TranslatableString(labelText)
 		
 	def satisfiedByNode(self, node):
 		# FIXME
 		if node.labeller:
-			return node.labeller.name==self.labelText
+			return stringMatches(self.labelText, node.labeller.name)
 		else: return False
 
 	def describeSearchResult(self):
-		return 'labelled "%s"'%self.labelText
+		return 'labelled %s'%self.labelText
 
 	def makeScriptMethodCall(self, isRecursive):
 		return "child(label='%s'%s)"%(self.labelText, makeScriptRecursiveArgument(isRecursive, True))
@@ -252,13 +261,13 @@ class IsLabelledAs(Predicate):
 class IsAMenuNamed(Predicate):
 	"""Predicate subclass that looks for a menu by name"""
 	def __init__(self, menuName):
-		self.menuName = menuName
+		self.menuName = TranslatableString(menuName)
 	
 	def satisfiedByNode(self, node):
-		return node.roleName=='menu' and node.name==self.menuName
+		return node.roleName=='menu' and stringMatches(self.menuName, node.name)
 
 	def describeSearchResult(self):
-		return '"%s" menu'%(self.menuName)
+		return '%s menu'%(self.menuName)
 
 	def makeScriptMethodCall(self, isRecursive):
 		return "menu('%s'%s)"%(self.menuName, makeScriptRecursiveArgument(isRecursive, True))
@@ -269,14 +278,14 @@ class IsAMenuNamed(Predicate):
 class IsAMenuItemNamed(Predicate):
 	"""Predicate subclass that looks for a menu item by name"""
 	def __init__(self, menuItemName):
-		self.menuItemName = menuItemName
+		self.menuItemName = TranslatableString(menuItemName)
 	
 	def satisfiedByNode(self, node):
 		roleName = node.roleName
-		return (roleName=='menu item' or roleName=='check menu item' or roleName=='radio menu item') and node.name==self.menuItemName
+		return (roleName=='menu item' or roleName=='check menu item' or roleName=='radio menu item') and stringMatches(self.menuItemName, node.name)
 
 	def describeSearchResult(self):
-		return '"%s" menuitem'%(self.menuItemName)
+		return '%s menuitem'%(self.menuItemName)
 
 	def makeScriptMethodCall(self, isRecursive):
 		return "menuItem('%s'%s)"%(self.menuItemName, makeScriptRecursiveArgument(isRecursive, True))
@@ -287,13 +296,13 @@ class IsAMenuItemNamed(Predicate):
 class IsATextEntryNamed(Predicate):
 	"""Predicate subclass that looks for a text entry by name"""
 	def __init__(self, textEntryName):
-		self.textEntryName = textEntryName
+		self.textEntryName = TranslatableString(textEntryName)
 	
 	def satisfiedByNode(self, node):
-		return node.roleName=='text' and node.name==self.textEntryName
+		return node.roleName=='text' and stringMatches(self.textEntryName, node.name)
 
 	def describeSearchResult(self):
-		return '"%s" textentry'%(self.textEntryName)
+		return '%s textentry'%(self.textEntryName)
 
 	def makeScriptMethodCall(self, isRecursive):
 		return "textentry('%s'%s)"%(self.textEntryName, makeScriptRecursiveArgument(isRecursive, True))
@@ -304,13 +313,13 @@ class IsATextEntryNamed(Predicate):
 class IsAButtonNamed(Predicate):
 	"""Predicate subclass that looks for a button by name"""
 	def __init__(self, buttonName):
-		self.buttonName = buttonName
+		self.buttonName = TranslatableString(buttonName)
 	
 	def satisfiedByNode(self, node):
-		return node.roleName=='push button' and node.name==self.buttonName
+		return node.roleName=='push button' and stringMatches(self.buttonName, node.name)
 
 	def describeSearchResult(self):
-		return '"%s" button'%(self.buttonName)
+		return '%s button'%(self.buttonName)
 
 	def makeScriptMethodCall(self, isRecursive):
 		return "button('%s'%s)"%(self.buttonName, makeScriptRecursiveArgument(isRecursive, True))
@@ -321,13 +330,13 @@ class IsAButtonNamed(Predicate):
 class IsATabNamed(Predicate):
 	"""Predicate subclass that looks for a tab by name"""
 	def __init__(self, tabName):
-		self.tabName = tabName
+		self.tabName = TranslatableString(tabName)
 	
 	def satisfiedByNode(self, node):
-		return node.roleName=='page tab' and node.name==self.tabName
+		return node.roleName=='page tab' and stringMatches(self.tabName, node.name)
 
 	def describeSearchResult(self):
-		return '"%s" tab'%(self.tabName)
+		return '%s tab'%(self.tabName)
 
 	def makeScriptMethodCall(self, isRecursive):
 		return "tab('%s'%s)"%(self.tabName, makeScriptRecursiveArgument(isRecursive, True))
Index: dogtail/tree.py
===================================================================
RCS file: /cvs/gnome/dogtail/dogtail/tree.py,v
retrieving revision 1.6
diff -u -p -r1.6 tree.py
--- dogtail/tree.py	17 Oct 2005 23:51:00 -0000	1.6
+++ dogtail/tree.py	21 Oct 2005 05:23:22 -0000
@@ -809,6 +809,24 @@ class Node:
 		also logs the search.
 		"""
 		return self.findChild (predicate.IsATabNamed(tabName=tabName), recursive)
+
+	def getUserVisibleStrings(self):
+		"""
+		Get all user-visible strings in this node and its descendents.
+		
+		(Could be implemented as an attribute)
+		"""
+		result=[]
+		if self.name:
+			result.append(self.name)
+		if self.description:
+			result.append(self.description)
+		try: 
+			children = self.children
+		except: return result
+		for child in children:
+				result.extend(child.getUserVisibleStrings())
+		return result
 		
 class Root (Node):
 	"""
Index: dogtail/apps/wrappers/evolution.py
===================================================================
RCS file: /cvs/gnome/dogtail/dogtail/apps/wrappers/evolution.py,v
retrieving revision 1.4
diff -u -p -r1.4 evolution.py
--- dogtail/apps/wrappers/evolution.py	17 Oct 2005 23:51:00 -0000	1.4
+++ dogtail/apps/wrappers/evolution.py	21 Oct 2005 05:23:22 -0000
@@ -18,8 +18,9 @@ from dogtail.apps.categories import *
 # The table rows have NODE_CHILD_OF relations (with paths, in at-poke)
 # 
 
+import dogtail.i18n
 
-
+dogtail.i18n.translationDb = dogtail.i18n.TranslationDatabaseFromPackageMoFiles('evolution', 'fr')
 
 # App-specific wrapper classes:
 

Attachment: i18n-test.py
Description: application/python



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]