Database backed i18n for java web-app - java

I'd like to use a database to store i18n key/value pairs so we can modify / reload the i18n data at runtime. Has anyone done this? Or does anyone have an idea of how to implement this? I've read several threads on this, but I haven't seen a workable solution.
I'm specifically refering to something that would work with the jstl tags such as
<fmt:setlocale>
<fmt:bundle>
<fmt:setBundle>
<fmt:message>
I think this will involve extending ResourceBundle, but when I tried this I ran into problems that had to do with the way the jstl tags get the resource bundle.

I finally got this working with danb's help above.
This is my resource bundle class and resource bundle control class.
I used this code from #[danb]'s.
ResourceBundle bundle = ResourceBundle.getBundle("AwesomeBundle", locale, DbResourceBundle.getMyControl());
javax.servlet.jsp.jstl.core.Config.set(actionBeanContext.getRequest(), Config.FMT_LOCALIZATION_CONTEXT, new LocalizationContext(bundle, locale));
and wrote this class.
public class DbResourceBundle extends ResourceBundle
{
private Properties properties;
public DbResourceBundle(Properties inProperties)
{
properties = inProperties;
}
#Override
#SuppressWarnings(value = { "unchecked" })
public Enumeration<String> getKeys()
{
return properties != null ? ((Enumeration<String>) properties.propertyNames()) : null;
}
#Override
protected Object handleGetObject(String key)
{
return properties.getProperty(key);
}
public static ResourceBundle.Control getMyControl()
{
return new ResourceBundle.Control()
{
#Override
public List<String> getFormats(String baseName)
{
if (baseName == null)
{
throw new NullPointerException();
}
return Arrays.asList("db");
}
#Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException,
InstantiationException, IOException
{
if ((baseName == null) || (locale == null) || (format == null) || (loader == null))
throw new NullPointerException();
ResourceBundle bundle = null;
if (format.equals("db"))
{
Properties p = new Properties();
DataSource ds = (DataSource) ContextFactory.getApplicationContext().getBean("clinicalDataSource");
Connection con = null;
Statement s = null;
ResultSet rs = null;
try
{
con = ds.getConnection();
StringBuilder query = new StringBuilder();
query.append("select label, value from i18n where bundle='" + StringEscapeUtils.escapeSql(baseName) + "' ");
if (locale != null)
{
if (StringUtils.isNotBlank(locale.getCountry()))
{
query.append("and country='" + escapeSql(locale.getCountry()) + "' ");
}
if (StringUtils.isNotBlank(locale.getLanguage()))
{
query.append("and language='" + escapeSql(locale.getLanguage()) + "' ");
}
if (StringUtils.isNotBlank(locale.getVariant()))
{
query.append("and variant='" + escapeSql(locale.getVariant()) + "' ");
}
}
s = con.createStatement();
rs = s.executeQuery(query.toString());
while (rs.next())
{
p.setProperty(rs.getString(1), rs.getString(2));
}
}
catch (Exception e)
{
e.printStackTrace();
throw new RuntimeException("Can not build properties: " + e);
}
finally
{
DbUtils.closeQuietly(con, s, rs);
}
bundle = new DbResourceBundle(p);
}
return bundle;
}
#Override
public long getTimeToLive(String baseName, Locale locale)
{
return 1000 * 60 * 30;
}
#Override
public boolean needsReload(String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime)
{
return true;
}
};
}

Are you just asking how to store UTF-8/16 characters in a DB? in mysql it's just a matter of making sure you build with UTF8 support and setting that as the default, or specifying it at the column or table level. I've done this in oracle and mysql before. Create a table and cut and paste some i18n data into it and see what happens... you might be set already..
or am I completely missing your point?
edit:
to be more explicit... I usually implement a three column table... language, key, value... where "value" contains potentially foreign language words or phrases... "language" contains some language key and "key" is an english key (i.e. login.error.password.dup)... language and key are indexed...
I've then built interfaces on a structure like this that shows each key with all its translations (values)... it can get fancy and include audit trails and "dirty" markers and all the other stuff you need to enable translators and data entry folk to make use of it..
Edit 2:
Now that you added the info about the JSTL tags, I understand a bit more... I've never done that myself.. but I found this old info on theserverside...
HttpSession session = .. [get hold of the session]
ResourceBundle bundle = new PropertyResourceBundle(toInputStream(myOwnProperties)) [toInputStream just stores the properties into an inputstream]
Locale locale = .. [get hold of the locale]
javax.servlet.jsp.jstl.core.Config.set(session, Config.FMT_LOCALIZATION_CONTEXT, new LocalizationContext(bundle ,locale));

We have a database table with key/language/term where key is a n integer and is a combined primary key together with language.
We are using Struts, so we ended up writing our own PropertyMessageResources implementation which allows us to do something like <bean:message key="impressum.text" />.
It works very well and gives us the flexibility to do dynamically switch languages in the front-end as well as updating the translations on the fly.

Actuly what ScArcher2 needed is davids response which is not marked a correct or helpfull.
The solution ScArcher2 chose to use is imo terrible mestake:) Loading ALL the translations at one time... in any bigger application its gonna kill it. Loading thousends of translations each request...
david's method is more commonly used in real production environments.
Sometimes to limit db calls, which is with every message translated, you can create groups of translations by topic, functionality etc. to preload them. But this is little bit more complex and can be substituted with good cache system.

Related

Reload ADF table after button click in a Fusion Web application

I recently started working on my first ADF project using JDeveloper 12c.
So, I have a Fusion Web Application that's connected to an Oracle database. On one of the jsf pages there are two ADF tables, that display data from the database. Below is a button that sends a "DELETE"-statement to the database to delete selected entries. For reasons both tables have to be refreshed after this (the deletion affects the shown entries of both tables).
After I was already really happy that the tables were displaying the correct data and the button did its thing too I quickly realized that if the database changes, the tables in the jsf page will not get refreshed automatically. I searched around the web a bit for a a good beginner level tutorial of how to refresh elements of a jsf page in an ADF Fusion application. Sadly I didn't found anything that gave me the key to it so far.
I found this article on the Oracle HQ page, but it also has the habit of using many propietary namings and is written in flowing text, so there is no code sample or snippet or similar in it, which made it difficult to follow for a rookie.
This is a snippet from my Managed java bean, where I store my button functionality:
public String update() {
getDBConnection c = new DBConnection();
Connection conn = c.getConn();
RowKeySet selectedEntries = getDetailTable().getSelectedRowKeys();
Iterator selectedEntryIter = selectedEntries.iterator();
DCBindingContainer bindings = (DCBindingContainer)BindingContext.getCurrent().getCurrentBindingsEntry();
DCIteratorBinding entryIter = bindings.findIteratorBinding("my_iterator");
RowSetIterator rSIter = entryIter.getRowSetIterator();
try {
PreparedStatement pstmt1 = conn.prepareStatement("DELETE ...");
PreparedStatement pstmt2 = conn.prepareStatement("DELETE ...");
while(selectedEntryIter.hasNext()){
Key key = (Key)((List)selectedEntryIter.next()).get(0);
Row currentRow = rSIter.getRow(key);
BigDecimal barcode = (BigDecimal) currentRow.getAttribute("id");
BigDecimal field1 = (BigDecimal) currentRow.getAttribute("field1");
BigDecimal field2 = (BigDecimal) currentRow.getAttribute("field2");
pstmt1.setBigDecimal(1, id);
pstmt1.setBigDecimal(2, field1);
pstmt2.setBigDecimal(1, id);
pstmt2.setBigDecimal(2, field2);
pstmt1.executeUpdate();
pstmt2.executeUpdate();
}
conn.commit();
//i guess here i have to trigger to refresh the tables but I have pretty to no clue of how to do that
//where do I have to set the functionality? I read sth about creating another bean in the "session" package
//but somehow i have to access the jsf i want to have refreshed. Where do I create that connection?
//even a simple example or a good reference to a tutorial would be helpful for me
} catch (SQLException e) {
System.out.println(e.getMessage());
}
return null;
}
Probably this question is a duplicate to an already existing one and i'm just too stupid to find it, but I will give it a try anyways. Thanks in advance!
In your case you can simply add a entryIter.executeQuery(); right after your conn.commit(); (You should also avoid running a direct sql delete and use the standard ADF BC DELETE https://o7planning.org/11489/create-update-and-delele-data-using-adf-form-in-adf-bc#a9769791)
But to answer the question title for future queries, below is an exemple of a simple refresh table button, easily reusable, that i usually add on my clients tables toolbar :
//in your jsff
<af:panelCollection id="pc" >
<f:facet name="menus"/>
<f:facet name="statusbar"/>
<f:facet name="toolbar">
<af:toolbar id="t1" flex="5">
<af:group id="g1">
<af:commandImageLink shortDesc="Reload" partialSubmit="true" actionListener="#{YOUR_SCOPE.YOUR_BEAN.refreshTable}"
icon="#{resource['images:YOUR_RELOAD_ICON.png']}">
<f:attribute name="tableIdToRefresh" value="YOUR_TABLE_ID"/>
</af:commandImageLink>
</af:group>
</af:toolbar>
</f:facet>
<af:table id="YOUR_TABLE_ID" value="#{bindings.YOUR_VO.collectionModel}" var="row" rows="#{bindings.YOUR_VO.rangeSize}"
selectedRowKeys="#{bindings.YOUR_VO.collectionModel.selectedRow}" selectionListener="#{bindings.YOUR_VO.collectionModel.makeCurrent}" rowSelection="single"
fetchSize="#{bindings.YOUR_VO.rangeSize}" filterModel="#{bindings.XxcnVieIntSearchVCQuery.queryDescriptor}"
queryListener="#{bindings.XxcnVieIntSearchVCQuery.processQuery}" varStatus="vs" >
<!-- COLUMNS -->
</af:table>
</af:panelCollection>
//in your bean #{YOUR_SCOPE.YOUR_BEAN.refreshTable}
public void refreshTable(ActionEvent actionEvent) {
//Get the attribute tableIdToRefresh value. I like to have it as a jsff attribute so i can easily reuse the button elsewhere
String tableToRefreshId = "" + ((RichCommandImageLink)actionEvent.getSource()).getAttributes().get("tableIdToRefresh");
if (sValeurCode != null) {
addPprToComponentById(tableToRefreshId);
//If it doesn't suffice (see below) you can use this :
//refreshTableIterator(tableToRefreshId);
}
}
public static void addPprToComponentById(String id) {
Object component = findComponentInRoot(id); //from the great JSFUtils library
if (component != null) {
AdfFacesContext.getCurrentInstance().addPartialTarget((UIComponent)component);
AdfFacesContext.getCurrentInstance().partialUpdateNotify((UIComponent)component);
}
}
/**
* Locate an UIComponent in view root with its component id. Use a recursive way to achieve this.
* #param id UIComponent id
* #return UIComponent object
*/
public static UIComponent findComponentInRoot(String id) {
UIComponent component = null;
if (id != null) {
FacesContext facesContext = FacesContext.getCurrentInstance();
if (facesContext != null) {
UIComponent root = facesContext.getViewRoot();
if (root != null) {
component = findComponent(root, id);
}
}
}
return component;
}
You can also use this addPPR logic for other components.
If a simple ppr doesn't suffice. You can force the table query execution with those :
public static void refreshTableIterator(String tableId) {
RichTable table = findComponentInRoot(tableId);
DCIteratorBinding treeIterator = null;
if (table != null) {
treeIterator = getTableIterator(table);
if (treeIterator != null) {
RowSetIterator rsi = treeIterator.getRowSetIterator();
treeIterator.executeQuery();
rsi.closeRowSetIterator();
}
}
}
public static DCIteratorBinding getTableIterator(RichTable table) {
DCIteratorBinding treeIterator = null;
CollectionModel model = (CollectionModel)table.getValue();
if (model != null) {
JUCtrlHierBinding treeBinding = (JUCtrlHierBinding)model.getWrappedData();
if (treeBinding != null) {
treeIterator = treeBinding.getDCIteratorBinding();
}
}
return treeIterator;
}

Android doesn't localize strings properly unless they are 'preferred' on the device. Is there a workaround?

In my Android app I want to give users the ability to change app language regardless of system settings. I've read multiple tutorials on the subject and arrived at solution that works... but only if all in-app languages are already on system's preferred languages list. This is the settings screen I'm talking about:
https://imgur.com/a/chXjhvv
If any of my in-app language is not there then default string resources are used by Android. Has anyone had the same problem? Are there any workarounds?
This is my setup:
1) I have three strings.xml files in three separate folders (values, values-en and values-ru)
2) My in-app language helper method looks like this:
#Nullable
public static Context getContextLocalized(#Nullable Context context) {
if (context != null) {
Configuration configuration = new Configuration(context.getResources().getConfiguration());
configuration.setLocale(getLocale());
return context.createConfigurationContext(configuration);
} else {
return null;
}
}
It basically takes the locale selected by the user (the getLocale() method) and creates a new context with that locale.
3) I have three predefined locales:
new Locale("pl", "PL")
new Locale("en", "GB")
new Locale("ru", "RU")
that user can choose from.
4) The way I use getContextLocalized() looks like this:
#NonNull
public static String getStringLocalized(#Nullable Context context, #StringRes int stringId, Object... args) {
Context contextLocalized = getContextLocalized(context);
if (contextLocalized != null) {
try {
return contextLocalized.getString(stringId, args);
} catch (Exception e) {
Log.e(TAG, "getStringLocalized: ERROR " +
contextLocalized.getResources().getResourceName(stringId) +
"! " + e.getMessage());
e.printStackTrace();
return "ERROR";
}
}
return "NULL";
}
This method is what I use in my Fragments and Activities to extract the desired string.
This solution works great if my system language list contains all my in-app languages. However, if I delete Russian from preferred languages list then my app reverts to Polish (which is stored in the default values folder) whenever user chooses Russian as his in-app language.
My min SDK version is 20. I tested this using API 26 emulator.
I have written this test method:
public static void test(#NonNull Context appContext) {
if (!BuildConfig.DEBUG) {
return;
}
Resources appRes = appContext.getResources();
Configuration appConfig = appRes.getConfiguration();
Log.d("LanguageUtilsTest", "DEFAULT -> " + appContext.getString(R.string.cancel));
Configuration configEN = new Configuration(appConfig);
configEN.setLocale(new Locale("en", "GB"));
Context contextEN = appContext.createConfigurationContext(configEN);
Log.d("LanguageUtilsTest", "EN -> " + contextEN.getString(R.string.cancel));
Configuration configRU = new Configuration(appConfig);
configRU.setLocale(new Locale("ru", "RU"));
Context contextRU = appContext.createConfigurationContext(configRU);
Log.d("LanguageUtilsTest", "RU -> " + contextRU.getString(R.string.cancel));
}
If my system language list looks like this: https://imgur.com/a/chXjhvv
The results are:
DEFAULT -> Anuluj
EN -> Cancel
RU -> Отменить
If I remove all languages except Polish (Polski).
The results are:
DEFAULT -> Anuluj
EN -> Anuluj
RU -> Anuluj
I've tried adding resConfigs "en", "ru" in my gradle but it didn't help.
Any advice?

spring boot 2 properties configuration

I have some code that works properly on spring boot prior to 2 and I find it hard to convert it to work with spring boot 2.
Can somebody assist?
public static MutablePropertySources buildPropertySources(String propertyFile, String profile)
{
try
{
Properties properties = new Properties();
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
// load common properties
PropertySource<?> applicationYamlPropertySource = loader.load("properties", new ClassPathResource(propertyFile), null);
Map<String, Object> source = ((MapPropertySource) applicationYamlPropertySource).getSource();
properties.putAll(source);
// load profile properties
if (null != profile)
{
applicationYamlPropertySource = loader.load("properties", new ClassPathResource(propertyFile), profile);
if (null != applicationYamlPropertySource)
{
source = ((MapPropertySource) applicationYamlPropertySource).getSource();
properties.putAll(source);
}
}
propertySources = new MutablePropertySources();
propertySources.addLast(new PropertiesPropertySource("apis", properties));
}
catch (Exception e)
{
log.error("{} file cannot be found.", propertyFile);
return null;
}
}
public static <T> void handleConfigurationProperties(T bean, MutablePropertySources propertySources) throws BindException
{
ConfigurationProperties configurationProperties = bean.getClass().getAnnotation(ConfigurationProperties.class);
if (null != configurationProperties && null != propertySources)
{
String prefix = configurationProperties.prefix();
String value = configurationProperties.value();
if (null == value || value.isEmpty())
{
value = prefix;
}
PropertiesConfigurationFactory<?> configurationFactory = new PropertiesConfigurationFactory<>(bean);
configurationFactory.setPropertySources(propertySources);
configurationFactory.setTargetName(value);
configurationFactory.bindPropertiesToTarget();
}
}
PropertiesConfigurationFactory doesnt exist anymore and the YamlPropertySourceLoader load method no longer accepts 3 parameters.
(the response is not the same either, when I have tried invoking the new method the response objects were wrapped instead of giving me the direct strings/integers etc...)
The PropertiesConfigurationFactory should be replaced with Binder class.
Binder class
Sample code:-
ConfigurationPropertySource source = new MapConfigurationPropertySource(
loadProperties(resource));
Binder binder = new Binder(source);
return binder.bind("initializr", InitializrProperties.class).get();
We were also using PropertiesConfigurationFactory to bind a POJO to a
prefix of the Environment. In 2.0, a brand new Binder API was
introduced that is more flexible and easier to use. Our binding that
took 10 lines of code could be reduced to 3 simple lines.
YamlPropertySourceLoader:-
Yes, this class has been changed in version 2. It doesn't accept the third parameter profile anymore. The method signature has been changed to return List<PropertySource<?>> rather than PropertySource<?>. If you are expecting single source, please get the first occurrence from the list.
Load the resource into one or more property sources. Implementations
may either return a list containing a single source, or in the case of
a multi-document format such as yaml a source for each document in the
resource.
Since there is no accepted answer yet, i post my full solution, which builds upon the answer from #nationquest:
private ConfigClass loadConfiguration(String path){
MutablePropertySources sources = new MutablePropertySources();
Resource res = new FileSystemResource(path);
PropertiesFactoryBean propFactory = new PropertiesFactoryBean();
propFactory.setLocation(res);
propFactory.setSingleton(false);
// resolve potential references to local environment variables
Properties properties = null;
try {
properties = propFactory.getObject();
for(String p : properties.stringPropertyNames()){
properties.setProperty(p, env.resolvePlaceholders(properties.getProperty(p)));
}
} catch (IOException e) {
e.printStackTrace();
}
sources.addLast(new PropertiesPropertySource("prefix", properties));
ConfigurationPropertySource propertySource = new MapConfigurationPropertySource(properties);
return new Binder(propertySource).bind("prefix", ConfigClass.class).get();
}
The last three lines are the relevant part here.
dirty code but this is how i solved it
https://github.com/mamaorha/easy-wire/tree/master/src/main/java/co/il/nmh/easy/wire/core/utils/properties

Load all ResourceBundles and pick correct resource based on key and Locale

Okay, so I have been given this challenge to implement a "Service" that loads in all the resources for all locales that we support. Then it should be possible to pick they resource from correct ResourceBundle based on the key and the current Locale. How can I achieve this?
So this is how I have made my solution, I have a Service called TranslationService
public class TranslationService {
private List<ResourceBundle> resourceBundles;
public TranslationService(final Locale locale) {
ResourceBundle messageTexts = ResourceBundle.getBundle("MessageTexts", locale, new Utf8Control());
ResourceBundle notificationTexts = ResourceBundle.getBundle("NotificationTexts", locale, new Utf8Control());
ResourceBundle generalTexts = ResourceBundle.getBundle("GeneralTexts", locale, new Utf8Control());
Collections.addAll(this.resourceBundles, messageTexts, notificationTexts, generalTexts);
}
public String getText(String key) {
String text = null;
for (ResourceBundle resourceBundle : resourceBundles) {
try {
text = resourceBundle.getString(key);
} catch (MissingResourceException e) {
// DO NOTHING: If the key is not found in first resource means not that it isn't in the next
// check all resources for the key.
}
}
if (text == null) {
log.error("Could not find key {} in any resource.", key);
}
return text;
}
}
So what I want to achieve is to be able to load in all the specified Bundles for all supported Locales so to say I want to load in on initialization ex the MessageTexts_en_GB.properties, MessageTexts_fr_FR.properties, MessageTexts_ja_JP.properties etc. And then based on what locale is used and what key I am sending in I should be able to tell which Bundle to look into for the key without looping through all my Bundles. So to say if I get the Locale for fr_FR and the key PUSH_NOTIFICATION_REMINDER, then I would know that I have to lookup for the text in NotificationTexts_fr_FR.properties without having to loop through all the resources as I am doing. So is it even possible to do it like this or do I have to loop through all my resources as I am doing now, and if loading resources for all Locales is possible how would I eventually need to handle property naming clashes so I do not get the wrong language property?
So I am working currently on a solution which looks like this:
public class MyResourceBundle {
private Map<Locale, Map<String, String>> localeResources;
public MyResourceBundle() {
localeResources = new HashMap<>();
for (LocaleConfig config : LocaleConfig.values()) {
Map<String, String> resources = new HashMap<>();
ResourceBundle systemTexts = ResourceBundle.getBundle("SystemTexts", config.getLocale());
ResourceBundle translationTexts = ResourceBundle.getBundle("TranslationTexts", config.getLocale());
Enumeration systemKeys = systemTexts.getKeys();
while (systemKeys.hasMoreElements()) {
String key = (String) systemKeys.nextElement();
resources.put(key, systemTexts.getString(key));
}
Enumeration translationKeys = translationTexts.getKeys();
while (translationKeys.hasMoreElements()) {
String key = (String) translationKeys.nextElement();
resources.put(key, translationTexts.getString(key));
}
localeResources.put(config.getLocale(), resources);
}
}
public String getText(Locale locale, String key) {
String text = null;
text = localeResources.get(locale).get(key);
if (text == null) {
String errorMessage = "Key: " + key + " does not exist for locale " + locale.toString();
throw new MissingResourceException(errorMessage, this.getClass().getName(), key);
}
return text;
}
}
LocaleConfig.java :
public enum LocaleConfig {
DANISH("da", "DK"),
ENGLISH("en", "GB")
;
private String language;
private String country;
LocaleConfig(String language, String country) {
this.language = language;
this.country = country;
}
public Locale getLocale() {
return new Locale(language, country);
}
}
SystemTexts_da_DK.properties:
PERSON_1=Hans Hansen
PERSON_2=Anders Andersen
PERSON_3=Ib Ibsen
REMINDER_MESSAGE=Husk at teste...
SystemTexts_en_GB.properties:
PERSON_1=Frank Testerson
PERSON_2=Julie Testerson
PERSON_3=Test Testerson
REMINDER_MESSAGE=Remember to test...
TranslationTexts_da_DK.properties:
NOTE_NEW=Ny
NOTE_SEEN=Set
TranslationTexts_en_GB.properties:
NOTE_NEW=New
NOTE_SEEN=Seen
Now I can just call:
public class MyResourceBundleExample {
public static void main(String[] args) {
Locale locale = new Locale("da", "DK");
MyResourceBundle myResourceBundle = new MyResourceBundle();
System.out.println(myResourceBundle.getText(locale, "PERSON_3"));
}
}
Initial tests on this solution show that this would work fine, but if anyone has a better solution or a more smooth way of doing this I am all ears :).

plugin.properties mechanism in eclipse RCP

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.
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.
/**
* 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);
}
}
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.
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.
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.

Categories