【问题标题】:Different facelets (for use in templates) in JSF 2 per locale每个语言环境的 JSF 2 中的不同 facelets(用于模板中)
【发布时间】:2013-02-27 12:34:26
【问题描述】:

我在某处有一个模板,其中包含 <ui:insert name="help_contents" /> 和一个定义 <ui:define name="help_contents><!-- actual contents --></ui:define> 的页面,其中定义中的内容应该是基于 JSF 的(不仅仅是普通的 html/xhtml),由 faces servlet 处理并基于不同在语言环境上。但我不想对资源包执行此操作,因为这将需要每个属性的大量文本,并且必须为散布在文本中的每个组件分解它。换句话说,我想要每个区域设置一个 facelet,然后根据活动区域设置包含正确的 facelet。

这基本上就是问题所在。为了其他人的搜索,下面的上下文,如果你已经理解我的意思,请跳过。

JSF 2 中的国际化在很大程度上非常容易。您制作一个或多个资源包,在 faces-config.xml 中声明它们,然后就可以使用这些属性了。但我觉得这样的属性文件只适用于短标签文本、列标题、可能带有几个参数的小消息……当涉及到大部分文本时,它们看起来很笨拙。特别是如果文本应该穿插有 XHTML 标记或 JSF 组件,在这种情况下,您需要将其分解得太多。

目前我正在开发一些使用 JSF 2 的 Web 应用程序,其中 PrimeFaces 作为组件包,它使用常规意义上的 i18n 资源包。但是各种视图需要一个帮助页面。我还想在这些帮助页面中使用 JSF/PrimeFaces 组件,以便填充的表格或对话框的示例看起来与视图本身相同。

但是,包含基于区域设置的合成内容似乎没有我想象的那么简单。我希望 XHTML 页面(facelets)带有像 _en 或 _fr 这样的语言环境后缀,并根据活动的语言环境选择正确的页面。如果不存在这样的页面,它应该默认为 _en 页面(或没有后缀的仅包含英文内容的页面)。从 facescontext 获取语言环境字符串不是问题,但检测页面是否存在似乎更难。有什么方法可以在 JSF 中或通过 EL 执行此操作,还是应该通过托管 bean 完成?也许为此编写一个自定义标签会很有用,但我不确定这需要做多少工作。

我确实找到了this related question,但这似乎只有在我不想注入纯 HTML 内容时才有用。我想包含带有 JSF 内容的页面,以便它们实际上由 JSF servlet 处理和呈现。

【问题讨论】:

  • 到目前为止,给出了两个完全错误的答案,这里有一个总结:OP 希望在<ui:include> 上自动选择正确的本地化包含文件。 OP 不想在每个请求的基础上重定向到一个完全独立的页面,例如重定向。所以过滤器和事件监听器完全没有问题。
  • 我已更改标题以表明我们正在谈论 facelets 和模板,而不是简单地指向整页。标题没有正确表达,我的错。

标签: java jsf jsf-2 internationalization facelets


【解决方案1】:

以下是我对您的问题的解决方案。它体积庞大,但已完成,内容丰富,据我所知,完整。有了它,您将能够根据当前语言从一系列以语言为后缀的视图中包含必要的视图。

我对您的设置的假设

  1. 您正在处理描述语言的语言环境,即采用Locale.ENGLISH 格式;
  2. 您选择的语言存储在会话范围的 bean 中;
  3. 您将国际化页面保留为以下格式:page.xhtmlpage_en.xhtmlpage_fr.xhtml 等;
  4. 默认语言为英语;
  5. 您的FacesServlet 映射到*.xhtml

我的解决方案的标准设置

会话范围的 bean,包含可用的语言和用户选择:

@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);
    }

}

语言环境的转换器:

@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.xhtml) 带有指向国际化页面的链接,并且能够通过下拉框更改当前语言:

<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>

基于多个页面的解决方案 - 每种语言一个

通过添加 _lang 后缀 (page-multiple.xhtml) 实现国际化的基页

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

国际化页面:

英文版 (page-multiple_en.xhtml):

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

对于法语 (page-multiple_fr.xhtml):

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

对于德语(无视图,模拟丢失文件)。

执行重定向的托管 bean:

@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);
    }

}

每次请求查看page-multiple.xhtml 时,它都会重定向到以语言为后缀的视图,或者如果找不到目标语言的视图,则会重定向到英文视图。当前语言取自会话范围的 bean,所有视图必须位于服务器上的同一文件夹中。当然,这可以重做,而是基于视图参数中定义的语言。目标页面可以使用合成。默认数据可以在不带后缀的视图中提供,preRenderView 侦听器不执行重定向。

作为备注,我的(三个)视图存储在网页的international/ 文件夹中。

基于单一页面的所有语言解决方案

虽然您的问题应该由以前的设置解决,但我想到了另一个想法,我将在下面描述。

有时,创建与支持的语言一样多的视图(+1 用于重定向)可能更容易,而是创建一个视图,根据当前选择的语言有条件地呈现其输出。

视图(page-single.xhtml,也位于服务器上的同一文件夹中)可能如下所示:

<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>

使用此视图,您可以指定内部的所有数据,有条件地仅呈现所需语言所需的数据或默认数据。

提供自定义资源解析器

资源解析器将根据视图的当前语言环境包含所需的文件。

资源解析器:

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;
    }

}

web.xml中启用解析器:

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

通过此设置,可以呈现以下视图:

使用&lt;ui:include&gt;的视图,其中的国际化包含将使用创建的//ml/前缀定义:

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

不会有page-include.xhtml,但会有每种语言的视图,例如:

page-include_en.xhtml:

<h:outputText value="Welcome" />

page-include_fr.xhtml:

<h:outputText value="Bienvenue" />

这样,解析器将根据当前语言环境选择正确的国际化包含视图。

【讨论】:

  • 在解决了冗长的问题之后,我了解到 OP 本质上是在询问如何在每个包含/组合的基础上执行该操作,例如 &lt;ui:include src="some_#{view.locale}.xhtml"/&gt;,而不是在每个请求的基础上。那么 prerenderview 监听器就完全不适合这个了。本质上,OP 需要一个自定义资源解析器或自定义标记处理程序(另请参阅我对 partlov 答案的第一条评论)。
  • 顺便说一下,&lt;ui:fragment&gt; 是视图渲染时间标签,而不是视图构建时间标签。 &lt;c:if&gt;/&lt;c:choose&gt; 最终会生成一个不那么笨拙/低效的组件树。
  • @BalusC 我真的不明白为什么我的解决方案不符合 OP 的需求。根据修改后的问题,我背诵:“我想要每个语言环境的 facelet”...“定义
    OP 确认我的理解完全正确。所以,你的解决方案是不合适的。使用资源解析器 :) (注意/提示:JSF 在内部对/resources 文件夹的默认资源处理程序(不,不是解析器)做同样的事情)
  • @BalusC 我一定会尝试您的建议并分享我的发现。但是我的(第一个)解决方案是否适合每页的基本国际化需求?
【解决方案2】:

您可以定义复合组件,例如,它将只是标准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>

创建托管 bean,命名为 myResolver,可以是 @ApplicationScoped,因为它是完全无状态的 resolve() 方法:

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;
  }
}

在这种情况下,只需将src 放到没有语言环境扩展的页面上,然后让方法解决这个问题:

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

由于我包括儿童,您可以将ui:param 传递给您,包括喜欢原创。

此外,对于那些不习惯根据语言环境(不仅仅是部分)解析整个页面的人来说,使用Filter 更容易。在doFilter() 方法中,您可以检查该资源是否存在以及是否将请求转发到另一个页面:

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...
  }
}

根据您的需要配置此Filter 的映射。

【讨论】:

  • 在解决了冗长的问题之后,我了解到 OP 本质上是在询问如何在每个包含/组合的基础上执行该操作,例如 &lt;ui:include src="some_#{view.locale}.xhtml"/&gt;,而不是在每个请求的基础上。过滤器则完全不适合此。本质上,OP 需要一个自定义资源解析器或者自定义标记处理程序。
  • 如果 OP 想要解决每个 ui:include 的问题,我真的还没有支持。这部分是您得出的结论:“包括基于语言环境的作文内容”吗?我将等待 OP 的反馈,如果是,我将删除答案。
  • @BalusC 完全正确。或者,可以从托管 bean 中获取 xhtml 页面名称吗?在那里检测文件的存在并不难(并缓存答案)。另外,很抱歉让问题比本质更长......我这样做的原因是我喜欢为未来的访问者提供答案的背景,这样他们就可以确定他们的挑战是否相同。我输入时会考虑 Google 搜索。
  • @partlov 是的,确实是 ui:include 的解析。这是基于模板的,因此帮助内容是通过 ui:define 与内容确定的。但是不要删除答案...这些cmets也很有用。
  • G_H,基于 cmets 编辑和改进问题。我会否决这个非答案,因为它根本没有回答具体问题。
【解决方案3】:

从此link @ SO 您可以动态包含内容(检查选中的答案)。在支持文件中,如果您有一个可以适当设置文件名的钩子,我认为这可以解决问题。

对此不确定,您可以检查一下,如果您可以将参数(即部分路径)传递给 EL 中的方法,其余部分可以在方法内部处理,例如构造完整路径、附加当前语言环境和检查文件是否存在。

希望这会有所帮助。

更新(回答评论):

是的。你可以看看链接JSF 2 fu, Part 2: Templating and composite components

【讨论】:

  • 这可能很有趣,并且类似于我在我的链接中的问题的答案。但我认为包含的内容不会通过 JSF servlet/controller/whatever 处理 JSF 内容并生成 (x)html。还是我错了?
猜你喜欢
相关资源
最近更新 更多
热门标签