Skip to content

Tutorial03

Vítor E. Silva Souza edited this page Apr 18, 2022 · 7 revisions

JButler has moved!

The new version of JButler is now hosted at Labes/UFES and there is a new tutorial as well. This repository contains an older version, if you're interested.


JButler Tutorial, Part 03: internationalize the application

We are getting closer to JButler, I promise! There's one more thing to do before actually implementing something: prepare the Web application for i18n (short for internationalization, because there are 18 letters between i and n). JButler assumes that i18n is being used, so it's better to do it from the start.

Here are the steps (which will be explained later):

  1. Right-click Java Resources/src/main/java and select New > Package, then create package br.ufes.informatica.oldenburg;

  2. Right-click the br.ufes.informatica.oldenburg package under Java Resources/src/main/java and select New > Other.... Choose General > File from the list and click Next. Create a file called faces.properties with the following contents (based on the JSF 2.3 specification, section 2.5.2.4):

    ##
    ## Standard localized error and informational messages from the JSF 2.3 specification 
    ## Language: American English
    ##
    javax.faces.component.UIInput.CONVERSION = {0}: Conversion error occurred
    javax.faces.component.UIInput.REQUIRED = {0}: Validation Error: Value is required
    javax.faces.component.UIInput.UPDATE = {0}: An error occurred when processing your submitted information
    javax.faces.component.UISelectOne.INVALID = {0}: Validation Error: Value is not valid
    javax.faces.component.UISelectMany.INVALID = {0}: Validation Error: Value is not valid
    javax.faces.converter.BigDecimalConverter.DECIMAL={2}: ''{0}'' must be a signed decimal number.
    javax.faces.converter.BigDecimalConverter.DECIMAL_detail={2}: ''{0}'' must be a signed decimal number consisting of zero or more digits, that may be followed by a decimal point and fraction. Example: {1}
    javax.faces.converter.BigIntegerConverter.BIGINTEGER={2}: ''{0}'' must be a number consisting of one or more digits.
    javax.faces.converter.BigIntegerConverter.BIGINTEGER_detail={2}: ''{0}'' must be a number consisting of one or more digits. Example: {1}
    javax.faces.converter.BooleanConverter.BOOLEAN={1}: ''{0}'' must be 'true' or 'false'.
    javax.faces.converter.BooleanConverter.BOOLEAN_detail={1}: ''{0}'' must be 'true' or 'false'. Any value other than
    'true' will evaluate to 'false'.
    javax.faces.converter.ByteConverter.BYTE={2}: ''{0}'' must be a number between -128 and 127.
    javax.faces.converter.ByteConverter.BYTE_detail={2}: ''{0}'' must be a number between -128 and 127. Example:
    {1}
    javax.faces.converter.CharacterConverter.CHARACTER={1}: ''{0}'' must be a valid character.
    javax.faces.converter.CharacterConverter.CHARACTER_detail={1}: ''{0}'' must be a valid ASCII character.
    javax.faces.converter.DateTimeConverter.DATE={2}: ''{0}'' could not be understood as a date.
    javax.faces.converter.DateTimeConverter.DATE_detail={2}: ''{0}'' could not be understood as a date. Example: {1}
    javax.faces.converter.DateTimeConverter.TIME={2}: ''{0}'' could not be understood as a time.
    javax.faces.converter.DateTimeConverter.TIME_detail={2}: ''{0}'' could not be understood as a time. Example: {1}
    javax.faces.converter.DateTimeConverter.DATETIME={2}: ''{0}'' could not be understood as a date and time.
    javax.faces.converter.DateTimeConverter.DATETIME_detail={2}: ''{0}'' could not be understood as a date and time. Example: {1}
    javax.faces.converter.DateTimeConverter.PATTERN_TYPE={1}: A 'pattern' or 'type' attribute must be specified to convert the value ''{0}''.
    javax.faces.converter.DoubleConverter.DOUBLE={2}: ''{0}'' must be a number consisting of one or more digits.
    javax.faces.converter.DoubleConverter.DOUBLE_detail={2}: ''{0}'' must be a number between 4.9E-324 and
    1.7976931348623157E308 Example: {1}
    javax.faces.converter.EnumConverter.ENUM={2}: ''{0}'' must be convertible to an enum.
    javax.faces.converter.EnumConverter.ENUM_detail={2}: ''{0}'' must be convertible to an enum from the enum that contains the constant ''{1}''.
    javax.faces.converter.EnumConverter.ENUM_NO_CLASS={1}: ''{0}'' must be convertible to an enum from the enum, but no enum class provided.
    javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail={1}: ''{0}'' must be convertible to an enum from the enum, but no enum class provided.
    javax.faces.converter.FloatConverter.FLOAT={2}: ''{0}'' must be a number consisting of one or more digits.
    javax.faces.converter.FloatConverter.FLOAT_detail={2}: ''{0}'' must be a number between 1.4E-45 and
    3.4028235E38 Example: {1}
    javax.faces.converter.IntegerConverter.INTEGER={2}: ''{0}'' must be a number consisting of one or more digits.
    javax.faces.converter.IntegerConverter.INTEGER_detail={2}: ''{0}'' must be a number between -2147483648 and 2147483647 Example: {1}
    javax.faces.converter.LongConverter.LONG={2}: ''{0}'' must be a number consisting of one or more digits.
    javax.faces.converter.LongConverter.LONG_detail={2}: ''{0}'' must be a number between -9223372036854775808 to
    9223372036854775807 Example: {1}
    javax.faces.converter.NumberConverter.CURRENCY={2}: ''{0}'' could not be understood as a currency value.
    javax.faces.converter.NumberConverter.CURRENCY_detail={2}: ''{0}'' could not be understood as a currency value. Example: {1}
    javax.faces.converter.NumberConverter.PERCENT={2}: ''{0}'' could not be understood as a percentage.
    javax.faces.converter.NumberConverter.PERCENT_detail={2}: ''{0}'' could not be understood as a percentage.
    Example: {1}
    javax.faces.converter.NumberConverter.NUMBER={2}: ''{0}'' is not a number.
    javax.faces.converter.NumberConverter.NUMBER_detail={2}: ''{0}'' is not a number. Example: {1}
    javax.faces.converter.NumberConverter.PATTERN={2}: ''{0}'' is not a number pattern.
    javax.faces.converter.NumberConverter.PATTERN_detail={2}: ''{0}'' is not a number pattern. Example: {1}
    javax.faces.converter.ShortConverter.SHORT={2}: ''{0}'' must be a number consisting of one or more digits.
    javax.faces.converter.ShortConverter.SHORT_detail={2}: ''{0}'' must be a number between -32768 and 32767 Example: {1}
    javax.faces.converter.STRING={1}: Could not convert ''{0}'' to a string.
    javax.faces.validator.BeanValidator.MESSAGE = {0}
    javax.faces.validator.DoubleRangeValidator.MAXIMUM = {1}: Validation Error: Value is greater than allowable maximum of \u2018\u2019{0}\u2019\u2019
    javax.faces.validator.DoubleRangeValidator.MINIMUM = {1}: Validation Error: Value is less than allowable minimum of \u2018\u2019{0}\u2019\u2019
    javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE = {2}: Validation Error: Specified attribute is not between the expected values of {0} and {1}.
    javax.faces.validator.DoubleRangeValidator.TYPE = {0}: Validation Error: Value is not of the correct type
    javax.faces.validator.LengthValidator.MAXIMUM = {1}: Validation Error: Length is greater than allowable
    maximum of \u2018\u2019{0}\u2019\u2019
    javax.faces.validator.LengthValidator.MINIMUM = {1}: Validation Error: Length is less than allowable minimum of \u2018\u2019{0}\u2019\u2019
    javax.faces.validator.LongRangeValidator.MAXIMUM = {1}: Validation Error: Value is greater than allowable maximum of \u2018\u2019{0}\u2019\u2019
    javax.faces.validator.LongRangeValidator.MINIMUM = {1}: Validation Error Value is less than allowable minimum of \u2018\u2019{0}\u2019\u2019
    javax.faces.validator.LongRangeValidator.NOT_IN_RANGE={2}: Validation Error: Specified attribute is not between the expected values of {0} and {1}.
    javax.faces.validator.LongRangeValidator.TYPE = {0}: Validation Error: Value is not of the correct type
  3. In that same package, create the messages.properties file with the following contents (most of it is for JButler later, only the last few items will be used in this part):

    ##
    ## Global resource bundle for a JButler project. 
    ## Language: American English
    ##
    
    # Messages for PrimeFaces components.
    primefaces.password.prompt = Please enter a password
    primefaces.password.weak = Weak
    primefaces.password.good = Good
    primefaces.password.strong = Strong
    
    # For JButler's base project decorator:
    jbutler.text.ajax.loading = Loading data, please wait...
    jbutler.text.ajax.error = There has been an error communicating with the server.
    jbutler.text.ajax.complete = Last communication with the server successful.
    
    # Generic messages for JButler's CRUD web pages:
    jbutler.crud.button.add = Add
    jbutler.crud.button.filter = Filter
    jbutler.crud.button.cancelFilter = Cancel
    jbutler.crud.button.clearFilter = Clear
    jbutler.crud.button.create = New
    jbutler.crud.button.retrieve = View
    jbutler.crud.button.update = Modify
    jbutler.crud.button.delete = Delete
    jbutler.crud.button.cancelDeletion = Cancel
    jbutler.crud.button.confirmDeletion = Confirm deletion
    jbutler.crud.button.cancel = Cancel
    jbutler.crud.button.save = Save
    jbutler.crud.button.back = Back
    jbutler.crud.help.hotkeys.filterFocus = focus the filter field
    jbutler.crud.help.hotkeys.clearFilter = clear current filter
    jbutler.crud.help.hotkeys.create = open the form for a new entry
    jbutler.crud.help.hotkeys.retrieve = open the details of the selected item
    jbutler.crud.help.hotkeys.update = open the form for modifying the selected item
    jbutler.crud.help.hotkeys.delete = add the selected item to the trash bin for deletion
    jbutler.crud.help.hotkeys.cancelDeletion = cancel deletion and restore the items in the trash bin
    jbutler.crud.help.hotkeys.confirmDeletion = confirm the deletion of the items in the trash bin
    jbutler.crud.help.hotkeys.focusFirstField = focus the first form field
    jbutler.crud.help.hotkeys.backToList = go back to the listing
    jbutler.crud.help.hotkeys.save = save changes and go back to the listing
    jbutler.crud.help.hotkeys.cancel = cancel the operation and go back
    jbutler.crud.help.hotkeys.back = go back
    jbutler.crud.hotkey.filterFocus = f
    jbutler.crud.hotkey.clearFilter = x
    jbutler.crud.hotkey.create = c
    jbutler.crud.hotkey.retrieve = r
    jbutler.crud.hotkey.update = u
    jbutler.crud.hotkey.delete = d
    jbutler.crud.hotkey.cancelDeletion = esc
    jbutler.crud.hotkey.confirmDeletion = y
    jbutler.crud.hotkey.focusFirstField = f
    jbutler.crud.hotkey.backToList = esc
    jbutler.crud.hotkey.cancel = esc
    jbutler.crud.hotkey.back = esc
    jbutler.crud.text.trashHeader = Items to delete:
    
    # JButler default formats:
    jbutler.format.date.java=dd/MM/yyyy
    jbutler.format.date.primefaces=99/99/9999
    jbutler.format.date.label=dd/mm/aaaa
    jbutler.format.datetime.java=dd/MM/yyyy HH:mm:ss
    jbutler.format.taxCode.primefaces=999.999.999-99
    jbutler.format.taxCode.label=\#\#\#.\#\#\#.\#\#\#-\#\#
    jbutler.format.zipCode.primefaces=99999-999
    jbutler.format.zipCode.label=\#\#\#\#\#-\#\#\#
    
    # JButler regular expressions and validator messages.
    jbutler.regex.email=([^.@]+)(\\.[^.@]+)*@([^.@]+\\.)+([^.@]+)
    jbutler.regex.confirmationCode=[\\w\\-]\{36\}
    jbutler.regex.email.message = This is not a valid email address.
    jbutler.regex.confirmationCode.message = This confirmation code given is not valid.
    
    # Global messages for the Web application.
    menu.home = Home
    text.developedBy = Developed by <a href="http://www.inf.ufes.br/~vitorsouza/">Prof. Vítor E. Silva Souza</a> for <a href="http://informatica.ufes.br/">PPGI/UFES</a>
    text.home.about01 = This Web application allows professors teaching teaching Research Methodology courses to simulate a workshop in which the students will submit papers and perform peer review.
    text.home.about02 = The application is named after <a href="https://en.wikipedia.org/wiki/Henry_Oldenburg">Henry Oldenburg</a> which, according to Wikipedia, is the creator of scientific peer review.
    title.home = Home
    title.home.description = Welcome to Oldenburg, the Workshop Simulator.
  4. Now open Deployed Resources/webapp/WEB-INF/faces-config.xml, go to the Source tab and add the i18n configuration for JSF under the <faces-config></faces-config> tag:

    <application>
    	<!-- Defines the resource bundle that contains the standard JSF messages (overriding the ones provided). -->
    	<message-bundle>br.ufes.informatica.oldenburg.faces</message-bundle>
    
    	<!-- Loads resource bundles for i18n messages and assigns names to them. -->
    	<resource-bundle>
    		<base-name>br.ufes.informatica.oldenburg.messages</base-name>
    		<var>msgs</var>
    	</resource-bundle>
    
    	<!-- Defines the default locale. -->
    	<locale-config>
    		<default-locale>en-US</default-locale>
    	</locale-config>
    </application>
  5. We can now internationalize our Web pages. Open the template file Deployed Resources/webapp/WEB-INF/templates/template.xhtml and replace the Home menu item:

    <span>Home</span>

    with:

    <span><h:outputText value="#{msgs['menu.home']}" /></span>
  6. Also replace some of the text at the footer:

    <ui:define name="footer">
        <h:outputText value="#{msgs['text.developedBy']}" escape="false" />
    	<div class="pull-right hidden-xs" style="color: gray"><i>1.0.0</i></div>
    </ui:define>
  7. Finally, open the home page Deployed Resources/webapp/index.xhtml and also internationalize it:

    <ui:define name="title">
        <h:outputText value="#{msgs['title.home']}" />
    </ui:define>
    
    <ui:define name="description">
        <h:outputText value="#{msgs['title.home.description']}" />
    </ui:define>
    
    <ui:define name="body">
    	<p><h:outputText value="#{msgs['text.home.about01']}" /></p>
    	
    	<p><h:outputText value="#{msgs['text.home.about02']}" escape="false" /></p>
    </ui:define>

Deploy the application to see if everything works. The result is the same as before, but we are now prepared to use JButler and to translate our website to other languages more easily.

How does i18n works in a Jakarta EE application?

In general, i18n in Java works by performing a process called externalizing strings. This means that the strings with the text that is shown in the user interface is taken from the UI component and placed in an external file with .properties extension called resource bundle.

The name of the resource bundles follow a standard format: basename_ll_CC.properties, in which ll is the ISO-3166 language code and CC is the ISO-3166 country code, both optional. When a resource bundle is requested, one provides its basename and the desired locale, and Java will search for the most specific bundle that satisfies the locale. For instance, if the requested locale is en-US (American English), then Java will look for basename_en_US.properties, then (if not found), basename_en.properties (generic English), then finally basename.properties (default bundle). So what we did in the steps above was to provide the default bundle, which happens to be written in en-US.

In Jakarta EE, with JSF, resource bundles are registered in the faces-config.xml file using their fully qualified name (FQN) and associated with a name. For instance, br.ufes.informatica.oldenburg.messages (i.e., messages.properties under package br.ufes.informatica.oldenburg) was associated with the name msgs. The configuration file also indicates the default locale for this Web application: <default-locale>en-US</default-locale>.

Then, at any Web page, we can use Expression Language to refer to text that should be extracted from that resource bundle, e.g.: <h:outputText value="#{msgs['title.home']}" />. In this example, JSF will request the resource bundle associated with the msgs name for the default locale and search for the key title.home inside the .properties file, rendering in the page the contents associated with that key.

If we want to translate our Web application, we just need to provide another copy of each .properties file with the appropriate suffix for language and country, then change the default locale of the application. It's also possible to change the locale only for a specific user, so each visitor of the website could ask for the UI in their own language. We could also detect the preferred language from the user's OS locale configuration. All of this is left as exercises for the interested reader...

Finally, JSF also outputs many different messages as result of conversion or validation (mostly errors during these processes). If we don't do anything, it will fetch the default messages from its own implementation and all of them will be in English, independently of the locale of the system. If we want to internationalize them as well, we need to provide the <message-bundle>br.ufes.informatica.oldenburg.faces</message-bundle> configuration pointing to a resource bundle and then we can provide translations for it.

Next: implement a simple CRUD feature