Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Different facelets (for use in templates) in JSF 2 per locale

I have a template somewhere that has <ui:insert name="help_contents" /> and a page that defines <ui:define name="help_contents><!-- actual contents --></ui:define>, where the contents in the definition should be JSF-based (not just plain html/xhtml), handled by the faces servlet and differing based on locale. But I don't want to do this with resource bundles, since that would require large swathes of text per property and having to break it up for every component that's interspersed with the text. In other words, I want a facelet per locale, and then include the right one based on the active locale.

That's basically the question. Context below for the sake of others who are searching, skip if you already understand what I mean.

Internationalization in JSF 2 is, for the most part, very easy. You make one or more resource bundles, declare those in your faces-config.xml and you're ready to use the properties. But such properties files are to my feeling only good for short label texts, column headers, small messages with maybe a couple of parameters in them... When it comes to large portions of text, they seem unwieldy. Especially if the text should be interspersed with XHTML tags or JSF components, in which case you'd need to break it up far too much.

Currently I'm working on some web application that uses JSF 2, with PrimeFaces as component bundle, which uses resource bundles for i18n in the regular sense. But various views need a help page. I'd like to use JSF/PrimeFaces components in those help pages as well, so that examples of populated tables or dialogs look the same as in the view itself.

However, including composition content based on locale appears to be less straightforward than I thought. I'd like to have XHTML pages (facelets) with locale suffixes like _en or _fr and select the right page based on the active locale. If no such page exists, it should default to the _en page (or one without a suffix that just includes the English content). Getting the locale string from the facescontext isn't a problem, but detecting whether a page exists seems harder. Is there some way to do this in JSF or via the EL, or should this be done through a managed bean? Maybe it could be useful to write a custom tag for this, but I'm not sure how much work this entails.

I did find this related question, but that only seems useful if I wan't to inject pure HTML content. I'd like to include pages with JSF content so they're actually handled and rendered by the JSF servlet.

like image 910
G_H Avatar asked Feb 27 '13 12:02

G_H


2 Answers

Below is my solution to your problem. It is bulky, but finished, informative and, as far as I can see, complete. With it you shall be able to incude the necessary view from a family of language-suffixed views, basing on the current language.

My assumptions about your setup

  1. You are dealing with locales that describe languages, i.e. are in Locale.ENGLISH format;
  2. You selected language is stored in a session scoped bean;
  3. You keep the internationalized pages in the following format: page.xhtml, page_en.xhtml, page_fr.xhtml, etc;
  4. Default language is English;
  5. Your FacesServlet is mapped to *.xhtml.

Standard settings for my solution

Session scoped bean, holding available languages and user selection:

@ManagedBean
@SessionScoped
public class LanguageBean implements Serializable {

    private List<Locale> languages;//getter
    private Locale selectedLanguage;//getter + setter

    public LanguageBean() {
        languages = new ArrayList<Locale>();
        languages.add(Locale.ENGLISH);
        languages.add(Locale.FRENCH);
        languages.add(Locale.GERMAN);
        selectedLanguage = Locale.ENGLISH;
    }

    public Locale findLocale(String value) {
        for(Locale locale : languages) {
            if(locale.getLanguage().equals(new Locale(value).getLanguage())) {
                return locale;
            }
        }
        return null;
    }

    public void languageChanged(ValueChangeEvent e){
        FacesContext.getCurrentInstance().getViewRoot().setLocale(selectedLanguage);
    }

}

Converter for a locale:

@ManagedBean
@RequestScoped
public class LocaleConverter implements Converter {

    @ManagedProperty("#{languageBean}")
    private LanguageBean languageBean;//setter

    public LocaleConverter() {   }

    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        if(value == null || value.equals("")) {
            return null;
        }
        Locale locale = languageBean.findLocale(value);
        if(locale == null) {
            throw new ConverterException(new FacesMessage("Locale not supported: " + value));
        }
        return locale;
    }

    public String getAsString(FacesContext context, UIComponent component, Object value) {
        if (!(value instanceof Locale) || (value == null)) {
            return null;
        }
        return ((Locale)value).getLanguage();
    }

}

Main view (main.xhtml) with links to internationalized pages and with ability to change current language via a dropdown box:

<f:view locale="#{languageBean.selectedLanguage}">
    <h:head>
        <title>Links to internationalized pages</title>
    </h:head>
    <h:body>
        <h:form>
            <h:selectOneMenu converter="#{localeConverter}" value="#{languageBean.selectedLanguage}" valueChangeListener="#{languageBean.languageChanged}" onchange="submit()">
                <f:selectItems value="#{languageBean.languages}"/>
            </h:selectOneMenu>
        </h:form>
        <br/>
        <h:link value="Show me internationalized page (single)" outcome="/international/page-single"/>
        <br/>
        <h:link value="Show me internationalized page (multiple)" outcome="/international/page-multiple"/>
    </h:body>
</f:view>

Solution based on multiple pages - one per language

Base page that is internationalized by adding _lang suffixes (page-multiple.xhtml)

<f:metadata>
    <f:event type="preRenderView" listener="#{pageLoader.loadPage}"/>
</f:metadata>

Internationalized pages:

For English (page-multiple_en.xhtml):

<h:head>
    <title>Hello - English</title>
</h:head>
<h:body>
    Internationalized page - English
</h:body>

For French (page-multiple_fr.xhtml):

<h:head>
    <title>Hello - Français</title>
</h:head>
<h:body>
    Page internationalisé - Français
</h:body>

For German (no view, simulation of missing file).

Managed bean that performs redirection:

@ManagedBean
@RequestScoped
public class PageLoader {

    @ManagedProperty("#{languageBean}")
    private LanguageBean languageBean;//setter

    public PageLoader() {   }

    public void loadPage() throws IOException {
        Locale locale = languageBean.getSelectedLanguage();
        FacesContext context = FacesContext.getCurrentInstance();
        ExternalContext external = context.getExternalContext();
        String currentPath = context.getViewRoot().getViewId();
        String resource = currentPath.replace(".xhtml", "_" + locale.toString() + ".xhtml");
        if(external.getResource(resource) == null) {
            resource = currentPath.replace(".xhtml", "_en.xhtml");
        }
        String redirectedResource = external.getRequestContextPath() + resource.replace(".xhtml", ".jsf");
        external.redirect(redirectedResource);
    }

}

Every time view page-multiple.xhtml is requested it is redirected to the language-suffixed views, or to the english view, if target language's view is not found. Current language is taken from session scoped bean, all views must be located in the same folder on server. Of course, that can be redone, basing on language defined in a view parameter instead. The target pages can use a composition. Default data can be served in a non-suffixed view with preRenderView listener not performing redirection.

As a remark, my (three) views were stored in international/ folder of web pages.

Solution based on a single page for all languages

Though your problem should be covered by the former setup, another idea came to mind, that I will describe below.

Sometimes it might be easier not to create as many views (+1 for redirection) as there are supported languages, but instead create a single view that will conditionally render its output, basing on the currently selected language.

The view (page-single.xhtml, located in the same folder on server as well) could look like:

<ui:param name="lang" value="#{languageBean.selectedLanguage}"/>
<ui:fragment rendered="#{lang == 'en'}">
    <h:head>
        <title>Hello - English</title>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF8" />
    </h:head>
    <h:body>
        Internationalized page - English
    </h:body>
</ui:fragment>
<ui:fragment rendered="#{lang == 'fr'}">
    <h:head>
        <title>Hello - Français</title>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF8" />
    </h:head>
    <h:body>
        Page internationalisé - Français
    </h:body>
</ui:fragment>
<ui:fragment rendered="#{(lang ne 'en') and (lang ne 'fr')}">
    <h:head>
        <title>Hello - Default</title>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF8" />
    </h:head>
    <h:body>
        Internationalized page - Default
    </h:body>
</ui:fragment>

With this view you specify all data inside, conditionally rendering only the data demanded by the needed language, or default data.

Providing for a custom resource resolver

Resourse resolver will include the needed file, basing on the current locale of the view.

Resource resolver:

public class InternalizationResourceResolver extends ResourceResolver {

    private String baseLanguage;
    private String delimiter;
    private ResourceResolver parent;

    public InternalizationResourceResolver(ResourceResolver parent) {
        this.parent = parent;
        this.baseLanguage = "en";
        this.delimiter = "_";
    }

    @Override
    public URL resolveUrl(String path) {
        URL url = parent.resolveUrl(path);
        if(url == null) {
            if(path.startsWith("//ml")) {
                path = path.substring(4);
                Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
                URL urlInt = parent.resolveUrl(path.replace(".xhtml", delimiter + locale.toString() + ".xhtml"));
                if(urlInt == null) {
                    URL urlBaseInt = parent.resolveUrl(path.replace(".xhtml", delimiter + baseLanguage + ".xhtml"));
                    if(urlBaseInt != null) {
                        url = urlBaseInt;
                    }
                } else {
                    url = urlInt;
                }
            }
        }
        return url;
    }

}

Enable the resolver in web.xml:

<context-param>
    <param-name>javax.faces.FACELETS_RESOURCE_RESOLVER</param-name>
    <param-value>i18n.InternalizationResourceResolver</param-value>
</context-param>

With this setup it is possible to render the following view:

View which uses <ui:include>, in which internatiaonalised includes will be defined with the created //ml/ prefix:

<f:view locale="#{languageBean.selectedLanguage}">
    <h:head>
    </h:head>
    <h:body>
        <ui:include src="//ml/international/page-include.xhtml" />
    </h:body>
</f:view>

There will be no page-include.xhtml, but there will be per language views, like:

page-include_en.xhtml:

<h:outputText value="Welcome" />

page-include_fr.xhtml:

<h:outputText value="Bienvenue" />

This way, the resolver will choose the right internationalized included view, basing on the current locale.

like image 68
skuntsel Avatar answered Sep 20 '22 01:09

skuntsel


You can define composite component, for example, which will be just facade for standard ui:include.

resources/myComponents/localeInclude.xhtml:

<cc:interface>
  <cc:attribute name="src" required="true" type="java.lang.String"/>
</cc:interface>

<cc:implementation>
  <ui:include src="#{myResolver.resolve(cc.attrs.src)}">
    <cc:insertChildren/>
  </ui:inclue>
</cc:implementation>

Create managed bean, named myResolver which can be @ApplicationScoped as it is completely stateless with resolve() method:

public String resolve(String src) {
  String srcWithoutExt = src.replace(".xhtml", "");
  FacesContext facesContext = FacesContext.getCurrentInstance();
  ServletContext servletContext = (ServletContext) facesContext.getExternalContext().getContext();
  Locale locale = facesContext.getViewRoot().getLocale();
  String localizedSrc = srcWithoutExt + "_" + locale.getLanguage();
  URL url = null;
  if (src.startsWith("/")) {
    url = facesContext.getExternalContext().getResource(localizedSrc + ".xhtml");
  } else {
    try {
      url = new URL((HttpServletRequest) request).getRequestURL(), localizedSrc + ".xhtml");
    } catch (Exception e) { /* Doesn't exist */ }
  }
  if (url != null) {
    return localizedSrc + ".xhtml";
  } else {
    return src;
  }
}

In this case, just put src to page without locale extensions and let the method resolve this:

<my:localeInclude src="myPage.xhtml/>

As I included childrens, you can pass ui:param to you include like to original.

Additionally, for those who just wont to resolve whole page according to locale (not just parts) it is easier to use Filter. In doFilter() method you can check if that resource exists and if not forward request to another page:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException {

  if (request.getServletContext().getResource(request.getRequestURI()) == null) {
    // Here your page doesn't exist so forward user somewhere else...
  }
}

Configure mapping for this Filter according to your needs.

like image 42
partlov Avatar answered Sep 21 '22 01:09

partlov