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