Jump to: navigation, search

Difference between revisions of "Babel / Runtime Translation Editor"

(The TranslatableNLS Class)
(Editors and Views)
Line 50: Line 50:
 
To allow translation of editors and views, the following steps must be taken by the developer.
 
To allow translation of editors and views, the following steps must be taken by the developer.
  
1. Use the UpdatableResourceBundle class provided by the Babel runtime instead of ResourceBundle provided by Java.   
+
1. Use the TranslatableResourceBundle class provided by the Babel runtime instead of ResourceBundle provided by Java.   
  
 
<code>
 
<code>
resourceBundle = (UpdatableResourceBundle)ResourceBundle.getBundle("com.acme.myApplication.Language",
+
resourceBundle = (TranslatableResourceBundle)ResourceBundle.getBundle("com.acme.myApplication.Language",
             new UpdatableResourceControl(getStateLocation()));
+
             new TranslatableResourceControl(getStateLocation()));
UpdatableResourceBundle.register(resourceBundle, getBundle());
+
TranslatableResourceBundle.register(resourceBundle, getBundle());
 
</code>
 
</code>
  
 
This is typically called when the plug-in is started.  Note that the location of the plug-in state is passed.  This is where the delta files are stored.  The delta files contain the differences between the resource bundles embedded in the plug-in and the actual text to be used.  The differences being changes either made by the user or obtained from a server running Aptana's software.
 
This is typically called when the plug-in is started.  Note that the location of the plug-in state is passed.  This is where the delta files are stored.  The delta files contain the differences between the resource bundles embedded in the plug-in and the actual text to be used.  The differences being changes either made by the user or obtained from a server running Aptana's software.
  
2. Define a LanguageSet object in the part.
+
2. Define a TranslatableSet object in the part.
  
 
<code>
 
<code>
private LocalizableTextSet localizableTextCollection = new LocalizableTextSet();
+
private TranslatableSet fTranslatableSet = new TranslatableSet();
 
</code>
 
</code>
  
3. All text used in the part should be first wrapped in an object that implements the ILocalizableText interface.  These objects contain information about the source of the text such as the contributing plug-in, the resource bundle, and the key.  The ILocalizableText implementation is then 'associated' with a control.
+
3. All text used in the part should be first wrapped in an object that implements the ITranslatableText interface.  These objects contain information about the source of the text such as the contributing plug-in, the resource bundle, and the key.  The ITranslatableText implementation is then 'associated' with a control.
  
The LocalizableText object can be used to provide an implementation of ILocalizableText where the text comes directly from a resource bundle.  The constructor needs the UpdatableResourceBundle and the key:
+
The TranslatableText object can be used to provide an implementation of ITranslatableText where the text comes directly from a resource bundle.  The constructor needs the TranslatableResourceBundle and the key:
  
 
<code>
 
<code>
ILocalizableText title = new LocalizedText(resourceBundle, "Title") //$NON-NLS-1$
+
ITranslatableText title = new TranslatableText(resourceBundle, "Title") //$NON-NLS-1$
 
</code>
 
</code>
  
This localizable text object is then 'associated' with a control.  This is done by creating an object derived from the abstract LocalizedTextInput class and providing an implementation of the updateControl method.  The updateControl method should set the text into the appropriate control.  So, for the title of a part, the following code may be used:
+
This translatable text object is then 'associated' with a control.  This is done by creating an object derived from the abstract TranslatableTextInput class and providing an implementation of the updateControl method.  The updateControl method should set the text into the appropriate control.  So, for the title of a part, the following code may be used:
  
localizableTextSet.associate(
+
fTranslatableSet.associate(
new LocalizedTextInput(title) {
+
new TranslatableTextInput(title) {
 
@Override
 
@Override
 
public void updateControl(String text) {
 
public void updateControl(String text) {
Line 85: Line 85:
 
);
 
);
  
The updateControl method is called initially and also whenever the user provides a different translation of the text.  Thus the user will see the changed text immediately.  This immediate feedback right into the user's running application is important to encourage contributions and also to ensure users know what they are changing.
+
The updateControl method is called initially when the TranslatableTextInput object is constructed and also whenever the user provides a different translation of the text.  Thus the user will see the changed text immediately.  This immediate feedback right into the user's running application is important to encourage contributions and also to ensure users know what they are changing.
  
 
Now suppose there are two possible titles to the view, depending, perhaps, on the state of a checkbox in the view.  Suppose both possible titles are taken from the resource bundle, and the title flips between them as the checkbox is checked.  The application, when changing the title, could make another called to 'associate'.  The problem is, there would then be two updateControl implementations, and the original would be called also.  To solve this issue, the 'associate' method has another form in which an object is passed as the first parameter.  You can pass any object, but it must be the same object each time 'associate' is called to set the title.  This object is used as the key is a map, and if the key matches a previous key then the entry is replaced.  In the above example of setting the title, you could use, say, a String object with a value of "title" as the key, or you could use the ViewPart object itself.
 
Now suppose there are two possible titles to the view, depending, perhaps, on the state of a checkbox in the view.  Suppose both possible titles are taken from the resource bundle, and the title flips between them as the checkbox is checked.  The application, when changing the title, could make another called to 'associate'.  The problem is, there would then be two updateControl implementations, and the original would be called also.  To solve this issue, the 'associate' method has another form in which an object is passed as the first parameter.  You can pass any object, but it must be the same object each time 'associate' is called to set the title.  This object is used as the key is a map, and if the key matches a previous key then the entry is replaced.  In the above example of setting the title, you could use, say, a String object with a value of "title" as the key, or you could use the ViewPart object itself.
  
It is a little long winded to provide an implementation of updateControl for every text message.  Other forms of the 'associate' method are provided that do this for you.  For example, to associate text with a Label, simply call  
+
It is a little long winded to provide an implementation of updateControl for every text message.  Other forms of the 'associate' method are provided in the TranslatableSet class that do this for you.  For example, to associate text with a Label, simply call  
  
 
<code>
 
<code>
associate(Label labelControl, ILocalizableText text)  
+
associate(Label labelControl, ITranslatableText text)  
 
</code>
 
</code>
  
Line 100: Line 100:
  
 
<code>
 
<code>
associateTooltip(Label labelControl, ILocalizableText tooltip)  
+
associateTooltip(Label labelControl, ITranslatableText tooltip)  
 
</code>
 
</code>
  
When a new translation is set into an active control, the container layout may need to be re-calculated.  If a dialog box, this may even cause the size of the dialog box to change.  This could be done in the updateControl implementations.  However, that would mean the implementations supplied by the LocalizableTextSet object cannot be used.  An alternative method is to implement the Layout method in the LocalizableTextSet object.  By implementing the layout in this method, the layout will be re-calculated only once even if the user changed multiple text values.
+
When a new translation is set into an active control, the container layout may need to be re-calculated.  If a dialog box, this may even cause the size of the dialog box to change.  This could be done in the updateControl implementations.  However, that would mean the implementations supplied by the TranslatableSet object cannot be used.  An alternative method is to implement the Layout method in the TranslatableSet object.  By implementing the layout in this method, the layout will be re-calculated only once even if the user changed multiple text values.
  
 
== Persistence ==
 
== Persistence ==

Revision as of 00:22, 7 March 2008

Babel Runtime Translation Editor

One of the aims of the Babel project is to make it is easy as possible for end users to contribute translations.

The Babel Runtime Plug-in

The Babel runtime plug-in (org.eclipse.babel.runtime) should be included in an Eclipse RCP application to allow users of that application to provide translations. Note that this is quite different from the Babel Resource Bundle Editor plug-in. The The Message Editor plug-in runs in the IDE only, and is used by the developer to aid in producing language files for the program under development. The runtime plug-in, in comparison, is included in the RCP application (the target configuration), and allows the end-user to provide translations. While end-users of the Eclipse IDE typically have a good understanding of plug-ins, Java resource bundles, and the localization architecture in general, end-users of RCP applications typically will have no such understanding. The Babel runtime plug-in therefore is oriented towards allowing the user to translate what the user sees on the screen at that moment, and hides from the user details such as the key and the plug-in that contributed the message.

Getting Started

Because readers will more likely be familiar with the Eclipse IDE than with any other single RCP application, the Eclipse IDE is used as the example target application in these steps. However, these steps can be applied to any RCP application.

1. Copy org.eclipse.babel.runtime_1.0.0.jar into your Eclipse 3.3 plugins directory.

2. For a more interesting demo, set the system Locale to a locale that has a country code as well as a language code, and install the language packs for the selected locale. If using Windows XP, go to the 'Advanced' tab of 'Regional and Language Options, select in 'Language for non-Unicode programs', and re-boot, or just start eclipse with the "-nl" parameter specified. In the following screen shots, we use FR_ca, so to see the same screens, start eclipse using "eclipse.exe -nl fr_CA".

3. Start up Eclipse.

The Babel runtime plug-in allows users to translate only to the language to which the system Locale is set. If their locale has both a language and a country code then they can provide both country specific translations and general translations for that language. For example, if the locale is fr_CA then the user can provide both translations specific to french Canadians and general french translations. The reason for restricting the user in this way is to make the UI as simple as possible for non-technical users. The intent is that the user is translating/correcting what the user sees on the screen. There is of course nothing to stop a user from changing the system locale and restarting the platform.

The plug-in does allow a way for the user to view and translate messages based on the contributing plug-in. However, as end-users are not likely to be aware of a particular message's contributing plug-in, the user can also view messages based on the part (editor or view) and the action bar (menu, toolbar, or status bar), or the dialog.

Having started the Eclipse IDE, open the "Translate Text..." menu under "Help". Select the "Menu" tab and you should see a list of menu items. This list should match the actual menu. (The only known difference being that items will appear in the list even if they have been excluded from the menu based on disabled activities). A few items may have 'lock' icons, which means the Babel runtime plug-in was not able to obtain sufficient information about the source of the message to allow translation. On any of the other items, you may provide translations by in-place editing. If you are interested, the icons have tooltips that show the source of the message. However, this information has been relegated to the tooltip because end-users have no need for this information and it probably would not mean anything to them.

Expand the "File" element in the tree and you should see a row for each of the menu items under the "File" menu. Try entering a translation for, say, the "Open" menu. Press OK. Now select the "File" menu and you should see your new translation taking immediate effect. Quit the workbench and re-start. You should see that you new translation is still active. This demonstrates that the plug-in allows the user to provide translations that are both hot-swappable and persistent across sessions.

The Babel runtime plug-in fully takes care of the menu. It could also fully take care of the toolbar and status bar though this has not been implemented so currently there is no support for providing translations of toolbar and status bar text (other than knowing the contributing plug-in, resource bundle, and key).

To allow translation of views, editors, and dialogs, changes must be made to the source code for those parts. Of course, none of the parts and dialogs in the IDE or in your RCP application have yet been modified to work with the Babel runtime. You will see a tab in the translation dialog with a title that matches the editor or view part that was active when you opened the dialog. If you select that tab, though, you will see only a message indicating that the part does not support dynamic text translation. However, there is one dialog that has been so modified. The dialog boxes used in Babel itself supports dynamic translation.

Workbench actions cannot be used while a dialog is open. Translatable dialog boxes therefore have a button in the lower left corner next to the help button. Press that to translate messages in the dialog box. You should then get another dialog box but with a tab titled 'Dialog - Text Translations'. Select that and you will see something like:

<embed babel-dialogTranslation.jpeg>

Enter your translations in the table. When you press 'OK', you should see the new text in the dialog.

You will notice one message that has the lock icon to indicate it is not translatable. That is because the message is in fact formatted from other component messages. Expand it and you will see the format string which you can edit and a parameter text which you cannot edit. The top level messages are what the user actually sees in the application. By doing this, instead of showing only the translatable parts, it is easier for the user to see the message that the user wants to translate.

As mentioned earlier, to allow translation of views, editors, and dialogs, changes must be made to the source code for those parts. We now describe the changes that the developer of a part must make to the code so that end-users can provide translations.

Editors and Views

To allow translation of editors and views, the following steps must be taken by the developer.

1. Use the TranslatableResourceBundle class provided by the Babel runtime instead of ResourceBundle provided by Java.

resourceBundle = (TranslatableResourceBundle)ResourceBundle.getBundle("com.acme.myApplication.Language",

 	          new TranslatableResourceControl(getStateLocation()));

TranslatableResourceBundle.register(resourceBundle, getBundle());

This is typically called when the plug-in is started. Note that the location of the plug-in state is passed. This is where the delta files are stored. The delta files contain the differences between the resource bundles embedded in the plug-in and the actual text to be used. The differences being changes either made by the user or obtained from a server running Aptana's software.

2. Define a TranslatableSet object in the part.

private TranslatableSet fTranslatableSet = new TranslatableSet();

3. All text used in the part should be first wrapped in an object that implements the ITranslatableText interface. These objects contain information about the source of the text such as the contributing plug-in, the resource bundle, and the key. The ITranslatableText implementation is then 'associated' with a control.

The TranslatableText object can be used to provide an implementation of ITranslatableText where the text comes directly from a resource bundle. The constructor needs the TranslatableResourceBundle and the key:

ITranslatableText title = new TranslatableText(resourceBundle, "Title") //$NON-NLS-1$

This translatable text object is then 'associated' with a control. This is done by creating an object derived from the abstract TranslatableTextInput class and providing an implementation of the updateControl method. The updateControl method should set the text into the appropriate control. So, for the title of a part, the following code may be used:

fTranslatableSet.associate( new TranslatableTextInput(title) { @Override public void updateControl(String text) { ViewPart.this.setText(text); } } );

The updateControl method is called initially when the TranslatableTextInput object is constructed and also whenever the user provides a different translation of the text. Thus the user will see the changed text immediately. This immediate feedback right into the user's running application is important to encourage contributions and also to ensure users know what they are changing.

Now suppose there are two possible titles to the view, depending, perhaps, on the state of a checkbox in the view. Suppose both possible titles are taken from the resource bundle, and the title flips between them as the checkbox is checked. The application, when changing the title, could make another called to 'associate'. The problem is, there would then be two updateControl implementations, and the original would be called also. To solve this issue, the 'associate' method has another form in which an object is passed as the first parameter. You can pass any object, but it must be the same object each time 'associate' is called to set the title. This object is used as the key is a map, and if the key matches a previous key then the entry is replaced. In the above example of setting the title, you could use, say, a String object with a value of "title" as the key, or you could use the ViewPart object itself.

It is a little long winded to provide an implementation of updateControl for every text message. Other forms of the 'associate' method are provided in the TranslatableSet class that do this for you. For example, to associate text with a Label, simply call

associate(Label labelControl, ITranslatableText text)

The first parameter is used both as the key in the map and also it the control into which the text is set.

Most methods are overloaded versions of 'associate'. However, some have different names to avoid ambiguity. For example, to associate text with the tooltip of a label control, you would use:

associateTooltip(Label labelControl, ITranslatableText tooltip)

When a new translation is set into an active control, the container layout may need to be re-calculated. If a dialog box, this may even cause the size of the dialog box to change. This could be done in the updateControl implementations. However, that would mean the implementations supplied by the TranslatableSet object cannot be used. An alternative method is to implement the Layout method in the TranslatableSet object. By implementing the layout in this method, the layout will be re-calculated only once even if the user changed multiple text values.

Persistence

If you restart Eclipse, you will see your text changes are still there. The messages are stored in a properties file in the runtime workspace.

The message changes should be communicated to the servers running Aptana's code contribution. This work has not been done. Before this can be done, a programmatic interface is required.

An update command should be added that fetches the latest translations from the server and updates the delta files.

The TranslatableNLS Class

A new class has been provided that replaces NLS. To provide translatable messages, extend from TranslatableNLS. Change all the messages from String objects to ITranslatableText objects. The bind methods also return ITranslatableText objects.

The initialization method is slightly different. Your class would typically contain the following code:

static { // A little risky using the plugin activator in a static initializer. // Let's hope it is in a good enough state. initializeMessages(BUNDLE_NAME, Messages.class, Activator.getDefault()); }

You will see an extra parameter. The plug-in activator is mainly required because getStateLocation is called to get the location in which the language delta files are maintained. The plug-in activator is also used to get the OSGi bundle id so that it can be displayed in the tooltip giving the origins of each text.

Implementation

The code to build the menu is one of the trickier parts of this plug-in.

1. It is possible to traverse the menu, but that gains us little as we have no means of determining where each text originated. We solve this by traversing the menu bar contribution tree. This is rather trickier because the tree does not map simply to the actual menu but needs some knowledge of the various contribution classes in order to get the menu tree to match the menu.

2. Having access to the contribution objects still does not get us to the origins of each piece of text. The problem is that the code to parse the plugin.xml files will replace any keys with the actual localized text. The way we get back to the key is to re-parse plugin.xml ourselves. This is obviously not the most efficient. Basically we know what elements to look for in the plugin.xml file based on the type of the menu contribution object.

3. The last problem concerns updating the menu dynamically after the user has edited a message. Sometimes this is as easy as calling 'setText' on the contribution item followed by a call to 'update'. Sometimes 'setText' is private, for some reason. It would be possible to update the underlying menu items, but that runs the risk that the change could be overwritten.

The above could all be made rather simpler if we were able to make changes to the core eclipse code. It will probably to a lot easier to get the changes into the core if the Babel runtime code is already working and the concept proven.