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

3

7

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.

Oxyacetylene answered 27/2, 2013 at 12:34 Comment(2)
Given two completely wrong answers on the question so far, here's a summary: the OP want to be to auto-select the right localized include file on <ui:include>. The OP do not want to redirect to a completely independent page on a per-request basis by e.g. a redirect. So a filter and event listener are completely out of question.Villanovan
I've changed the title to indicate we're talking about facelets and templating, rather than simply directing to full pages. The title just didn't properly express that, my bad.Oxyacetylene
R
3

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.

Ruling answered 27/2, 2013 at 19:54 Comment(8)
After boiling down the long winded question, I understood that the OP is essentially asking how to perform that on a per include/composition basis like so <ui:include src="some_#{view.locale}.xhtml"/>, not on a per-request basis. A prerenderview listener is then completely insuitable to this. Essentially, the OP needs a custom resource resolver or perhaps a custom taghandler (see also my 1st comment on partlov's answer).Villanovan
By the way, <ui:fragment> is a view render time tag, not a view build time tag. A <c:if>/<c:choose> would end up in a less clumsy/inefficient component tree.Villanovan
@Villanovan I don't actually understand why my solution doesn't fit OP's needs. As per the revised question, I recite: "I want a facelet per locale" ... "that defines <ui:define name="help_contents><!-- actual contents --></ui:define>". In other words, he would like to have per-language views and use templating. Isn't that right?Ruling
OP confirmed that my understanding was exactly right. So, your solution is insuitable. Play around with a resource resolver :) (note/hint: JSF is internally doing the same with the default resource handler (no, not resolver) for /resources folder)Villanovan
@Villanovan I'll definitely try what you suggest and share my findings. But does my (first) solution suit basic internationalization needs on a per page basis?Ruling
@Villanovan The ResourceResolver denies to resolve the url in the subsequent requests and does so only for the first time the page is accessed. Is it an expected behaviour or am I missing something there?Ruling
@Villanovan Can you take a quick glance at the solution which uses the custom resource resolver? Basically, it does the job correctly on the first access to the page, but later the ResourceResolver#resolveUrl doesn't get fired for <ui:include> component. Can you give a hint on why this is happening?Ruling
The work you've put in this answer is greatly appreciated! Using a resource resolver might be an idea. But since the contents are loaded via ajax only once the help overlay is brought up, it might be an issue if only the first access is resolved. Maybe worth experimenting with this. It could be possible that simply using a bean that determines the right value for the <ui:include> src attribute might suffice. Right now, language isn't selectable and simply determined by the request header, thus taken from the faces context. Could change later, but this isn't really important.Oxyacetylene
B
2

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.

Bergeron answered 27/2, 2013 at 12:52 Comment(6)
After boiling down the long winded question, I understood that the OP is essentially asking how to perform that on a per include/composition basis like so <ui:include src="some_#{view.locale}.xhtml"/>, not on a per-request basis. A filter is then completely insuitable to this. Essentially, the OP needs a custom resource resolver or perhaps a custom taghandler.Villanovan
I'm really not yet shore if OP wants to resolve per ui:include. Is this part where you concluded this: "including composition content based on locale"? I'll just wait for OP's feedback and if so I'll delete answer.Bergeron
@Villanovan Entirely correct. Alternatively, could getting the xhtml page name from a managed bean work? It wouldn't be hard to detect the existence of the file there (and cache the answer). Also, sorry for making the question longer than the essence... The reason I do this is that I like to provide context for future visitors to the answer so they can determine if their challenge is the same. I type with Google searches in mind.Oxyacetylene
@Bergeron Yes, it is indeed resolution by ui:include. This is template-based so the help content is determine via a ui:define with the contents. But don't delete the answer... These comments are useful too.Oxyacetylene
G_H, Edit and improve the question based on comments. I'd have downvoted this non-answer as it does not answer the concrete question at all.Villanovan
@Villanovan Thanks for the tag correction. I'll shorten the answer to something more concrete. Mind that I'm rather new to JSF and am currently only taking care of this portion of the app, so I'm learning as I go.Oxyacetylene
D
1

From this link @ SO you can include a content dynamically (check the checked answer). In the backing file if you have a hook where you can appropriately set filename, I think that can do the trick.

Not sure about this, you can check, if you can pass argument i.e. partial path to the method in EL, rest can be handled inside the method like constructing full path ,appending current locale and checking file is present or not.

Hope this helps.

Update(To answer the comment):

Yes it would. You can have a look at link JSF 2 fu, Part 2: Templating and composite components

Deepset answered 27/2, 2013 at 12:58 Comment(1)
That could be interesting and is similar to the answer for the question I linked in mine. But I don't think the included content would go through the JSF servlet/controller/whatever processes JSF content and generates (x)html. Or am I mistaken in this?Oxyacetylene

© 2022 - 2024 — McMap. All rights reserved.