0

My problem is related to localization, I have JPA entities having fields like fieldnameEN,fieldnameFR,fieldnameRU. I want to write some getter method which automatically detects current locale and returns appropriate field. I'm using facelets and such getter will help me to call getter and delegate localization issues to backend. Locale already stored in session and getting it is not a problem. I know how do such method manually, something like

getProperty(){
locale = takeFromSession();
if(locale=en) return getPropertyEN();
if(locale=fr) return getPropertyFR();
}

But I want to keep DRY principle with the help of AspectJ or some interceptor.

Current thoughts on implementation - in each getter call determine running method name, pass Object state to some interceptor and in interceptor perform locale checks and return appropriate value of field which is already passed to interceptor.

Is there any working example of solving such problem?

How pass object state to interceptor?

Are there any better approaches to solve my problem?

UPDATE

Kriegaex recommended to use bundles. Actually, we use bundles for markup (headers and captions) but we also need to localize entities stored in database. Bundles require usage of 'hash tags' as keys in .property files, but I don't want to store entity values as hash tags. Bundles will force users to fill business values as 'hash tags' and I wouldn't like that)

Even if use english values as key or some hash from values we need to make 100 queries if entity has 100 properties. And yes, I mean 100 DB queries, because AFAIK bundle stored in RAM which may be insufficient to store translations that's why in our case bundles should be in key-value DB.

About recompilation - most probably we will have only 3 languages and don't need scale in such direction.

If somebody know answer to my topic question, please share some little example)

Community
  • 1
  • 1

2 Answers2

0

AspectJ is not intended to be used to patch up bad application design. I could easily tell you how to write some cheap aspect code using reflection in order to call the right getter for the current language, but

  • it is ugly,
  • it is slow,
  • having one property + getter per language and encoding the language ID in the method name does not scale. If you want to add another language, you will have to add fields to dozens or hundreds of entities.

Maybe you should consider using a standard means like resource bundles for your property names. This way you can change text constants or even add new languages without recompiling the code. Because internationalisation is a cross-cutting concern, you can then still use AspectJ in order to declare access methods for your translations via ITD (inter-type definition) or by some other means, if you want to keep them out of the core code. That way your core code could be totally language-agnostic.


Update:

Anyway, if you want it so much, here is a sample showing you what you can do with AOP, namely with AspectJ. The solution proposed by user gknicker is similar, but it only works for one class. Mine keeps the code separate in an aspect and can apply it to many classes at once.

The plan is to manually annotate each entity class containing multi-language field captions with a marker annotation. I made up one called @Entity. Alternatively, you could also determine the target classes by their superclass or by a class or package name pattern, AspectJ is very powerful in this regard. As I said, it is just an example.

In the next step we will define an aspect which does the following:

  • Define an interface LocalisedCaption.
  • Define a few sample default methods using reflection magic in order to
    • get the localised caption for one field,
    • get all localised captions for all defined entity fields,
    • get a map of localised captions and field values for an entity instance.
  • Use ITD (inter-type declaration) in order to make all @Entity classes implement that interface and thus inherit its methods.

Last, but not least, we will use the new methods in from sample application.

package de.scrum_master.app;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Entity {}
package de.scrum_master.app;

@Entity
public class Person {
    public static final String firstNameEN = "first name";
    public static final String firstNameFR = "prénom";
    public static final String firstNameRU = "и́мя";

    public static final String lastNameEN = "last name";
    public static final String lastNameFR = "nom de famille";
    public static final String lastNameRU = "фами́лия";

    private String firstName;
    private String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + firstName + ", lastName=" + lastName + "]";
    }
}
package de.scrum_master.aspect;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import de.scrum_master.app.Application;
import de.scrum_master.app.Entity;

public aspect EntityCaptionLocaliser {
    public interface LocalisedCaption {
        String getCaption(String attributeName);
    }

    declare parents :
        @Entity * implements LocalisedCaption;

    public String LocalisedCaption.getCaption(String attributeName)
        throws ReflectiveOperationException
    {
        String fieldName = attributeName + Application.locale;
        Field field = getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return (String) field.get(this);
    }

    public Map<String, String> LocalisedCaption.getAllCaptions()
            throws ReflectiveOperationException
        {
            Map<String, String> captions = new HashMap<>();
            for (Field field : getClass().getDeclaredFields()) {
                if (Modifier.isStatic(field.getModifiers()))
                    continue;
                String attributeName = field.getName();
                captions.put(attributeName, getCaption(attributeName));
            }
            return captions;
        }

    public Map<String, Object> LocalisedCaption.getCaptionValuePairs()
            throws ReflectiveOperationException
        {
            Map<String, Object> captions = new HashMap<>();
            for (Field field : getClass().getDeclaredFields()) {
                if (Modifier.isStatic(field.getModifiers()))
                    continue;
                field.setAccessible(true);
                String attributeName = field.getName();
                captions.put(getCaption(attributeName), field.get(this));
            }
            return captions;
        }
}
package de.scrum_master.app;

public class Application {
    public static String locale = "EN";

    public static void main(String[] args) throws Exception {
        Person albert = new Person("Albert", "Einstein");
        System.out.println("Showing localised captions for " + albert + ":");
        locale = "EN";
        System.out.println(albert.getAllCaptions());
        System.out.println(albert.getCaptionValuePairs());
        locale = "FR";
        System.out.println(albert.getAllCaptions());
        System.out.println(albert.getCaptionValuePairs());
        locale = "RU";
        System.out.println(albert.getAllCaptions());
        System.out.println(albert.getCaptionValuePairs());
    }
}

Console output for Application.main:

Showing localised captions for Person [firstName=Albert, lastName=Einstein]:
{lastName=last name, firstName=first name}
{first name=Albert, last name=Einstein}
{lastName=nom de famille, firstName=prénom}
{nom de famille=Einstein, prénom=Albert}
{lastName=фами́лия, firstName=и́мя}
{фами́лия=Einstein, и́мя=Albert}
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • 1
    I still think it is a bad idea to do it this way, but see my updated answer for an AOP-style solution. – kriegaex Dec 11 '14 at 13:04
  • Cool answer, thanks. If I get it correctly, I still need to add one line of code in each getName, getAge, or getSmth like `getSmth(){return getCaption("smth")}` am I right? I think it's impossible to just write `getSmth()` and magically inject some code, which will detect smth in runtime, and return appropriate property? Is it possible to add some aspect to each getter which will determine automatically method name(I know how to do it) and force this method aspect to pass detected attr name to your getCaption()? It looks like if it's possible it wil be nested aspect)) –  Dec 12 '14 at 05:06
  • I think `getSmth()` should return the value of an entity's property `smth` and not its caption, see my example above. Besides that, why do you want to create methods for everything? Isn't that bloat? The captions are `static final` constants, you can easily make the `public` just like I did. But if you want to get accessor methods for them, maybe a code generation step similar to what AspectJ can do via [annotation processing](http://andrewclement.blogspot.de/2014/08/annotation-processing-in-ajdt.html) might be the way to go. I still think this kind of architecture is a nightmare. – kriegaex Dec 12 '14 at 13:18
0

Better to use standard Java i18n.

However, if you insist on pursuing your current design, here's an example using reflection.

public class Scratch
{
    public static void main(String[] args)
    {
        System.out.println(new Scratch().getProperty());
    }

    String propertyEN = "want";
    String propertyFR = "voulez";

    public String getProperty()
    {
        return (String)getForLocale("property");
    }

    private Object getForLocale(String attributeName)
    {
        String fieldName = attributeName + getLocale();
        try {
            Field field = getClass().getDeclaredField(fieldName);
            return field.get(this);
        } catch (ReflectiveOperationException e) {
            return e.getMessage();
        }
    }

    private String getLocale()
    {
        return "EN"; // takeFromSession();
    }
}
gknicker
  • 5,509
  • 2
  • 25
  • 41