views:

2486

answers:

5

My project includes multiple plugins and every plugin includes the plugin.properties file with near to 20 translations. The MANIFEST.MF file defines the name of the properties files where the external plugin strings are stored.

Bundle-Localization: plugin

The name of the plugin i define like

%plugin.name

Eclipse will search the "%plugin.name" in the plugin.properties file at runtime.

Which class read out the MANIFEST.MF Bundle-Localization entry and at which point is the string with the starting '%' suffix is searched in the "plugin.properties" file?

I want to find and patch these class in that way, that i can first look into some other directories/files for the "%plugin.name" identifier. With these new mechanism i can add fragments to my product and overwrite single lines in a "plugin.properties" file without changing the original plugin. With these mechanism i could create a build process for multiple customers just by adding different fragments. The fragments including the customer names and special string they want to change.

I want to do it that way, because the fragment mechanism only add files to the original plugin. When the "plugin.properties" file is existing in the plugin, the fragment "plugin.properties" files are ignored.

UPDATE 1:

The method

class ManifestLocalization{
...
protected ResourceBundle getResourceBundle(String localeString) {
}
...
}

returns the ResourceBundle of the properties file for the given locale string. When somebody nows how i can now first look into the fragment to get the resource path please post it.

UPDATE 2:

The method in class ManifestLocalization

    private URL findInResolved(String filePath, AbstractBundle bundleHost) {

     URL result = findInBundle(filePath, bundleHost);
     if (result != null)
      return result;
     return findInFragments(filePath, bundleHost);
    }

Searchs for the properties file and cache it. The translations can than get from the cached file. The problem is, that the complete file is cached and not single translations.

A solution would be to first read the fragment file, than read the bundle file. When both files are existing merge them into one file and write the new properties file to the disk. The URL of the new properties file returns, so that the new propetries file can cached.

A: 

Change the name of your fragment plugin.properties to something else eg. fragment.properties

in your fragment manifest change the Bundle-Localization: plugin to Bundle-Localization: fragment

Your plugin will be activated twice, the first time using the plugin.properties, the second using the fragment.properties.

But i can not use the same key for a different value. Only the plugin key is find and returned. When the searched key is find in the plugin the fragmet is never open
Markus Lausberg
A: 

Plugin activation is handled by the OSGi runtime Equinox. However I would strongly discourage trying to patch any files there to create specific behavior. The suggested way from Mark seems a much more sane approach to your problem.

lothar
Do you mean the suggested way from Mark Miller or what Mark? :)
Peteter
+2  A: 

Although I got the information wrong ... I had exactly the same problem. The plugin is not activated twice and I cannot get to the fragments Bundle-Localization key.

I want all my language translations in the plugin.properties (I know this is frowned upon but it is much easier to manage a single file).

I (half)solved the problem by using

public void populate(Bundle bundle) {
    String localisation = (String) bundle.getHeaders().get("Bundle-Localization");
    Locale locale = Locale.getDefault();

    populate(bundle.getEntry(getFileName(localisation)));
    populate(bundle.getEntry(getFileName(localisation, locale.getLanguage())));
    populate(bundle.getEntry(getFileName(localisation, locale.getLanguage(), locale.getCountry())));
    populate(bundle.getResource(getFileName("fragment")));
    populate(bundle.getResource(getFileName("fragment", locale.getLanguage())));
    populate(bundle.getResource(getFileName("fragment", locale.getLanguage(), locale.getCountry())));
}

and simply call my fragment localisation file name 'fragment.properties'.

This is not particularly elegant, but it works.

By the way, to get files from the fragment you need the getResource, it seems that fragment files are on the classpath, or are only searched when using getResource.

If someone has a better approach, please correct me.

All the best,

Mark.

Mark Miller
Where i can find the populate method? Or do i have to write it?
Markus Lausberg
Can you please put more informations into your answer. Thank you!
Markus Lausberg
+1  A: 
/**
 * The Hacked NLS (National Language Support) system.
 * <p>
 * Singleton.
 * 
 * @author mima
 */
public final class HackedNLS {
    private static final HackedNLS instance = new HackedNLS();

    private final Map<String, String> translations;

    private final Set<String> knownMissing;

    /**
     * Create the NLS singleton. 
     */
    private HackedNLS() {
     translations = new HashMap<String, String>();
     knownMissing = new HashSet<String>();
    }

    /**
     * Populates the NLS key/value pairs for the current locale.
     * <p>
     * Plugin localization files may have any name as long as it is declared in the Manifest under
     * the Bundle-Localization key.
     * <p>
     * Fragments <b>MUST</b> define their localization using the base name 'fragment'.
     * This is due to the fact that I have no access to the Bundle-Localization key for the
     * fragment.
     * This may change.
     * 
     * @param bundle The bundle to use for population.
     */
    public void populate(Bundle bundle) {
     String baseName = (String) bundle.getHeaders().get("Bundle-Localization");

     populate(getLocalizedEntry(baseName, bundle));
     populate(getLocalizedEntry("fragment", bundle));
    }

    private URL getLocalizedEntry(String baseName, Bundle bundle) {
     Locale locale = Locale.getDefault();
     URL entry = bundle.getEntry(getFileName(baseName, locale.getLanguage(), locale.getCountry()));
     if (entry == null) {
      entry = bundle.getResource(getFileName(baseName, locale.getLanguage(), locale.getCountry()));
     }
     if (entry == null) {
      entry = bundle.getEntry(getFileName(baseName, locale.getLanguage()));
     }
     if (entry == null) {
      entry = bundle.getResource(getFileName(baseName, locale.getLanguage()));
     }
     if (entry == null) {
      entry = bundle.getEntry(getFileName(baseName));
     }
     if (entry == null) {
      entry = bundle.getResource(getFileName(baseName));
     }
     return entry;
    }

    private String getFileName(String baseName, String...arguments) {
     String name = baseName;
     for (int index = 0; index < arguments.length; index++) {
      name += "_" + arguments[index];
     }
     return name + ".properties";
    }

    private void populate(URL resourceUrl) {
     if (resourceUrl != null) {
      Properties props = new Properties();
      InputStream stream = null;
      try {
       stream = resourceUrl.openStream();
       props.load(stream);
      } catch (IOException e) {
       warn("Could not open the resource file " + resourceUrl, e);
      } finally {
       try {
        stream.close();
       } catch (IOException e) {
        warn("Could not close stream for resource file " + resourceUrl, e);
       }
      }
      for (Object key : props.keySet()) {
       translations.put((String) key, (String) props.get(key));
      }
     }
    }

    /**
     * @param key The key to translate.
     * @param arguments Array of arguments to format into the translated text. May be empty.
     * @return The formatted translated string.
     */
    public String getTranslated(String key, Object...arguments) {
     String translation = translations.get(key);
     if (translation != null) {
      if (arguments != null) {
       translation = MessageFormat.format(translation, arguments);
      }
     } else {
      translation = "!! " + key;
      if (!knownMissing.contains(key)) {
       warn("Could not find any translation text for " + key, null);
       knownMissing.add(key);
      }
     }
     return translation;
    }

    private void warn(String string, Throwable cause) {
     Status status;
     if (cause == null) {
      status = new Status(
        IStatus.ERROR, 
        MiddlewareActivator.PLUGIN_ID, 
        string);
     } else {
      status = new Status(
       IStatus.ERROR, 
       MiddlewareActivator.PLUGIN_ID, 
       string,
       cause);
     }
     MiddlewareActivator.getDefault().getLog().log(status);

    }

    /**
     * @return The NLS instance.
     */
    public static HackedNLS getInstance() {
     return instance;
    }

    /**
     * @param key The key to translate.
     * @param arguments Array of arguments to format into the translated text. May be empty.
     * @return The formatted translated string.
     */
    public static String getText(String key, Object...arguments) {
     return getInstance().getTranslated(key, arguments);
    }
}
Mark Miller
The getLocalizedEntry bundle.getResource and bundle.getEntry are required as files on the classpath (those found in the fragment) need to the getResource whereas those in the plugin root (the plugin localization) require getEntry.Again, if anyone has a better answer please let us know as I would also be interested.
Mark Miller
I'm unclear how to use the class you have provided. Could it be used to translate for instance a perspective name declared in a plugin.xml file, in a different way than looking in the bundle.properties file?
Peteter
A: 

One way is to attach a bundle listener, and listen for installations of bundles (and perhaps also look at already installed bundles) and for each bundle generate/provide - and install - a fragment with the wanted property files. If this is done before the application starts up, this should have effect.

Peteter