@Messages

There's a new way of working with bundle keys in NetBeans Platform 7.0, as described in NbBundle.Messages. I've been converting tutorials to 7.0 (e.g., the NetBeans Plugin Quick Start and the NetBeans Platform Quick Start have already been done) and using this new @Messages annotation and really liking it.

The current tutorial I am working on, the NetBeans Platform Paint Tutorial makes a lot of use of it too.

For example, in the Paint Tutorial for 7.0, you have this annotation:

@Messages({"# {0} - image","UnsavedImageNameFormat=Image {0}"})
public PaintTopComponent() {
    setDisplayName(UnsavedImageNameFormat(ct++));
}

That replaces this:

String displayName = NbBundle.getMessage(
        PaintTopComponent.class,
        "UnsavedImageNameFormat",
        new Object[] { new Integer(ct++) }
        );
setDisplayName(displayName);

The "ct" referred to above is:

private static int ct = 0; //A counter you use to provide names for new images

At this point, I am able to save the file, the red error marks disappear (definitely in the latest dev builds), and then I can switch to the Files window and then browse to the generated "build/classes-generated/org/netbeans/paint/Bundle.java" file. There I find a static string for each key=value pair in my @Messages annotations. In the case of the above, I see:

static String UnsavedImageNameFormat(Object image) {
    return org.openide.util.NbBundle.getMessage(Bundle.class, "UnsavedImageNameFormat", image);
}

Then, when I browse to "build/classes/org/netbeans/paint", I see that a Bundle.properties file has been created, which has, among other keys, these:

# {0} - image
UnsavedImageNameFormat=Image {0}

I can see that this approach has a number of advantages, especially that I can stay within my Java source file while coding, i.e., no need to switch to a properties file because that file will be generated. I asked Jesse (who created the annotation) to list what he sees as the advantages and here they are:

  1. I only need to use Java, no more need to switch to a Bundle.properties file. Similarly, a diff of changes will show only a change block in a single file, whereas formerly you would see a change to \*.java and then a seemingly disconnected change in a Bundle.properties. This makes the diff harder to review, and with branchy development potentially harder to merge (someone else might have added completely unrelated keys to the same part of the same Bundle.properties).

  2. The generated Bundle.properties centralizes all messages from a package. As a matter of style I would put @Messages on the closest element to its usage. So if a key is used only in one method, put it on that method. (If it is used in multiple elements in a class, put it on the class.)

    Your @Messages annotations can be moved very close to their actual usage. If the language supported it, we would just write something like

    setDisplayName(@Localized("node_label") "My Node");

    and some magical source transformation would replace the string constant with a call to NbBundle; unfortunately this is not possible. But at least the messages can be within a few code lines of their usage. This makes it easy to see (or edit) the actual value while reading the code; formerly you needed a special tool (Java Ext Editor) to "see" localized values during code browsing, which only worked in the IDE and only with this plugin installed.

  3. Compile-time checking is supported in the IDE. That means you cannot use a key which does not exist, perhaps due to a typo - no more MissingResourceException's coming from rarely encountered code paths. Nor can you accidentally define a key twice (with same or different values), which was a fairly frequent occurrence in NB sources. Nor can you accidentally forget a message format param and have it come out as "{3}" in the output, or accidentally insert an extra param which was not read - again common mistakes in NB sources.

    Another rarer issue was people computing keys rather than using string constants to make code a little shorter, e.g. NbBundle.getMessage(X.class, "LBL_" + type + "_node"). Nice enough when it is right, but produces an MRE if the 'type' starts taking on an unforeseen value after some refactoring. With @Messages, you have to use a switch statement (or similar) on the 'type' - more verbose perhaps, but forces you to pay attention to the possibility that it has some unexpected value, and react gracefully.

  4. No more forgetting to delete Bundle strings. Another (very!) common mistake in NB sources is to delete a block of code, or even a whole class, but forget to delete all the bundle keys that it referenced - so you can look around Bundle.properties files and see labels for UI elements which have not existed in years. Similarly, after moving code to another location, developers would often leave behind unused copies of the old messages; or forget to move the messages, causing MREs until found by trial and error; or move messages which were in fact also used by other code in the original package, causing that to throw MRE.

    Preventing these issues required that you search for all usages of NbBundle in any code you were deleting or moving, collect a list of all the referenced keys, Ctrl-F search for all mentions of those keys in the package, and deal with them (delete or move or copy). And even being that careful could fail if there were computed keys (as above) - or if people used NbBundle.getMessage(ClassInAnotherPackage.class, "...") so that your naive text search missed some usages! All these problems are impossible if @Messages is used exclusively: keys are only produced corresponding to live Java code, and can only be consumed when they really exist (by code in the same package as the definition).

    Compare that to the benefits of statically typed languages in general - i.e. all of the above mistakes applied to plain old method calls are common enough when using, say, Python. Proponents of dynamic languages point out that you can catch most such mistakes by diligent testing, but I have seen plenty of runtime errors of this kind in released versions of Mercurial despite a big test suite. Anyway forgoing the benefits of a compiler is like not wearing a seatbelt because you are convinced you are a perfect driver!

  5. A side note is that if you see some bug in the UI and want to find what code implements it, formerly you would need to search for the UI label, then open the Bundle.properties defining it, copy the key, then search again (within the same package) for usages of the key. Now you can just search for the UI label and be taken directly to the Java code defining it.
Jesse ends with: "The processor for @Messages does stretch the limits of what JSR 269 can do (and what the IDE's editor can handle), and there is a small runtime cost (one extra Bundle.class loaded per package using @Messages), but the advantages in ease of use and reliability outlined above outweigh these drawbacks in my mind."

Finally, this annotation should have no adverse impact on localization since the output JAR still has a Bundle.properties per package with the same contents as before, including comments directed at localizers.

Comments:

Yeah ! I have to admit this is the first annotation I really understand and like and certainly will use. In general (most likely out of ignorance) I am concerned with all those annotations and that my Java code will look more like a scripting language than Java. But this one is really nice - thanx Jesse !

Posted by Bernd Ruehlicke on March 26, 2011 at 12:37 AM PDT #

Can you explain how localization works with the @messages annotation? Where should the localized bundle be placed?

Posted by Chris on April 10, 2011 at 12:49 AM PDT #

Is it possible to use this annotation to localize text inside initComponent() without requiring custom code? The problem is that using custom code the designer will only show "User Code". To avoid that, i'm using a mixed approach at the moment: i'm using the annotation to generate key/value pair, but inside initComponent() method i'm still using NbBundle.getMessage(...). This breaks one of the annotation's primary objectives: compile time safety. TIA

Posted by metator on May 17, 2011 at 01:09 AM PDT #

I'm sorry. That mixed approach i was talking about... doesn't work.

Posted by metator on May 17, 2011 at 01:18 AM PDT #

This example isn't working in NB 7.01
it gives me an underline error in setDisplayName then prompts me to create a method UnsavedImageNameFormat that takes an int.

Posted by Abaddon on September 29, 2011 at 12:27 AM PDT #

I'm also seeing that problem:

PaintApp\Paint\src\org\netbeans\paint\PaintTopComponent.java:45: cannot find symbol
symbol : method LBL_Clear()
location: class org.netbeans.paint.PaintTopComponent

The same goes for LBL_Foreground, LBL_Brushsize, and UnsavedImageNameFormat.

Posted by Michael on October 24, 2011 at 03:49 PM PDT #

Post a Comment:
  • HTML Syntax: NOT allowed
About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
12
13
14
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today