Internacionalização e localização em Java 8
1. Visão geral
Internationalization is a process of preparing an application to support various linguistic, regional, cultural or political-specific data. É um aspecto essencial de qualquer aplicativo multilíngue moderno.
Para leitura adicional,, devemos saber que há uma abreviação muito popular (provavelmente mais popular do que o nome real) para internacionalização -i18n devido às 18 letras entre 'i' e 'n'.
É crucial para os programas empresariais atuais servirem pessoas de diferentes partes do mundo ou de várias áreas culturais. Regiões culturais ou linguísticas distintas não determinam apenas descrições específicas do idioma, mas também moeda, representação numérica e até mesmo composição divergente de data e hora.
Por exemplo, vamos nos concentrar em números específicos de cada país. Eles têm vários separadores decimais e mil:
-
102.300,45 (Estados Unidos)
-
102 300,45 (Polônia)
-
102.300,45 (Germany)
Também existem diferentes formatos de data:
-
Segunda-feira, 1 de janeiro de 2018 15:20:34 CET (Estados Unidos)
-
domingo 1 de janeiro 2018 15 h 20 CET (França).
-
2018 年 1 月 1 下午 下午 03 时 20 分 34 秒 CET (China)
Além disso, diferentes países têm símbolos de moeda exclusivos:
-
£ 1.200,60 (Reino Unido)
-
€ 1.200,60 (Itália)
-
1 200,60 € (França)
-
$1,200.60 (United States)
Um fato importante a saber é que, mesmo que os países possuam a mesma moeda e símbolo de moeda - como França e Itália -, a posição do símbolo de moeda pode ser diferente.
2. Localização
Em Java, temos um recurso fantástico à nossa disposição, chamado de classeLocale.
Ele nos permite diferenciar rapidamente as localidades culturais e formatar nosso conteúdo adequadamente. É essencial para dentro do processo de internacionalização. Assim como i18n, Localização também tem sua abreviatura -*l10n*.
A principal razão para usarLocale é que toda a formatação específica de localidade necessária pode ser acessada sem recompilação. Um aplicativo pode manipular vários códigos de idioma ao mesmo tempo, portanto, o suporte ao novo idioma é simples.
As localidades são geralmente representadas por idioma, país e abreviação de variante, separadas por um sublinhado:
-
de (alemão)
-
it_CH (italiano, Suíça)
-
en_US_UNIX (Estados Unidos, plataforma UNIX)
2.1. Campos
Já aprendemos queLocale consists of language code, country code, and variant. There are two more possible fields to set: script and extensions.
Vamos dar uma olhada em uma lista de campos e ver quais são as regras:
-
Language pode ser um códigoISO 639 alpha-2 or alpha-3 ou uma subetiqueta de idioma registrado.
-
Region (Country) é o código do paísISO 3166 alpha-2 ou o código de áreaUN numeric-3.
-
Variant é um valor que diferencia maiúsculas de minúsculas ou um conjunto de valores que especifica uma variação deLocale.
-
Script deve ser um códigoISO 15924 alpha-4 válido.
-
Extensions é um mapa que consiste em chaves de um único caractere e valoresString.
OIANA Language Subtag Registry contém valores possíveis paralanguage,region,variantescript.
Não existe uma lista de valoresextension possíveis, mas os valores devem ser subtagsBCP-47 bem formados. As chaves e os valores são sempre convertidos em minúsculas.
2.2. Locale.Builder
Existem várias maneiras de criar objetosLocale. Uma forma possível utilizaLocale.Builder. Locale.Builder tem cinco métodos setter que podemos usar para construir o objeto e, ao mesmo tempo, validar esses valores:
Locale locale = new Locale.Builder()
.setLanguage("fr")
.setRegion("CA")
.setVariant("POSIX")
.setScript("Latn")
.build();
A representaçãoString deLocale acima éfr_CA_POSIX # Latn_.
É bom saber quesetting ‘variant' may be a little bit tricky as there’s no official restriction on variant values, although the setter method requires it to be BCP-47 compliant.
Caso contrário, ele lançaráIllformedLocaleException.
No caso em que precisamos usar um valor que não passa na validação, podemos usar construtoresLocale, pois eles não validam os valores.
2.3. Construtores
Locale tem três construtores:
-
novo local (linguagem String)
-
novo local (idioma da string, país da string)
-
novo local (idioma da string, país da string, variante da string)
Um construtor de 3 parâmetros:
Locale locale = new Locale("pl", "PL", "UNIX");
Umvariant válido deve ser umString de 5 a 8 alfanuméricos ou numérico único seguido por 3 alfanuméricos. Só podemos aplicar "UNIX" ao campovariant apenas por meio do construtor, pois não atende a esses requisitos.
No entanto, há uma desvantagem de usar construtores para criar objetosLocale - não podemos definir extensões e campos de script.
2.4. Constantes
Esta é provavelmente a maneira mais simples e limitada de obterLocales. A classeLocale tem várias constantes estáticas que representam o país ou idioma mais popular:
Locale japan = Locale.JAPAN;
Locale japanese = Locale.JAPANESE;
2.5. Tags de idioma
Outra maneira de criarLocale é chamar o método estático de fábricaforLanguageTag(String languageTag). Este método requer umString que atenda ao padrãoIETF BCP 47.
É assim que podemos criar o UKLocale:
Locale uk = Locale.forLanguageTag("en-UK");
2.6. Localidades disponíveis
Mesmo que possamos criar várias combinações de objetosLocale, podemos não ser capazes de usá-los.
Uma observação importante a ser observada é que osLocales em uma plataforma dependem daqueles que foram instalados no Java Runtime.
Como usamosLocales para formatação, os diferentes formatadores podem ter um conjunto ainda menor deLocales disponíveis que são instalados no Runtime.
Vamos verificar como recuperar matrizes de localidades disponíveis:
Locale[] numberFormatLocales = NumberFormat.getAvailableLocales();
Locale[] dateFormatLocales = DateFormat.getAvailableLocales();
Locale[] locales = Locale.getAvailableLocales();
Depois disso, podemos verificar se nossoLocale reside entre osLocales. disponíveis
Devemos lembrar quethe set of available locales is different for various implementations of the Java Platformand various areas of functionality.
A lista completa de localidades com suporte está disponível emthe Oracle’s Java SE Development Kit webpage.
2.7. Localidade padrão
Enquanto trabalhamos com localização, podemos precisar saber qual é oLocale padrão em nossa instânciaJVM. Felizmente, existe uma maneira simples de fazer isso:
Locale defaultLocale = Locale.getDefault();
Além disso, podemos especificar umLocale padrão chamando um método setter semelhante:
Locale.setDefault(Locale.CANADA_FRENCH);
É especialmente relevante quando desejamos criar testesJUnit que não dependem de uma instânciaJVM.
3. Números e moedas
Esta seção refere-se a formatadores de números e moedas que devem estar em conformidade com diferentes convenções específicas de localidade.
Para formatar tipos de números primitivos (int,double), bem como seus equivalentes de objeto (Integer,Double), devemos usar a classeNumberFormat e sua classe estática métodos de fábrica.
Dois métodos são interessantes para nós:
-
NumberFormat.getInstance(Locale locale)
-
NumberFormat.getCurrencyInstance(Locale locale)
Vamos examinar um exemplo de código:
Locale usLocale = Locale.US;
double number = 102300.456d;
NumberFormat usNumberFormat = NumberFormat.getInstance(usLocale);
assertEquals(usNumberFormat.format(number), "102,300.456");
Como podemos ver, é tão simples quanto criarLocalee usá-lo para recuperar a instância deNumberFormate formatar um número de amostra. Podemos notar quethe output includes locale-specific decimal and thousand separators.
Aqui está outro exemplo:
Locale usLocale = Locale.US;
BigDecimal number = new BigDecimal(102_300.456d);
NumberFormat usNumberFormat = NumberFormat.getCurrencyInstance(usLocale);
assertEquals(usNumberFormat.format(number), "$102,300.46");
A formatação de uma moeda envolve as mesmas etapas da formatação de um número. A única diferença é que o formatador anexa o símbolo da moeda e arredonda a parte decimal para dois dígitos.
4. Data e hora
Agora, vamos aprender sobre a formatação de datas e horas, que provavelmente é mais complexa do que formatar números.
Em primeiro lugar, devemos saber que a formatação de data e hora mudou significativamente no Java 8, pois contémDate/Time API completamente nova. Portanto, vamos examinar diferentes classes de formatador.
4.1. DateTimeFormatter
Since Java 8 was introduced, the main class for localizing of dates and times is the DateTimeFormatter class. Ele opera em classes que implementam a interfaceTemporalAccessor, por exemplo,LocalDateTime,LocalDate, LocalTime ouZonedDateTime. Para criar umDateTimeFormatter, devemos fornecer pelo menos um padrão, e então Locale. Vamos ver um código de exemplo:
Locale.setDefault(Locale.US);
LocalDateTime localDateTime = LocalDateTime.of(2018, 1, 1, 10, 15, 50, 500);
String pattern = "dd-MMMM-yyyy HH:mm:ss.SSS";
DateTimeFormatter defaultTimeFormatter = DateTimeFormatter.ofPattern(pattern);
DateTimeFormatter deTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.GERMANY);
assertEquals(
"01-January-2018 10:15:50.000",
defaultTimeFormatter.format(localDateTime));
assertEquals(
"01-Januar-2018 10:15:50.000",
deTimeFormatter.format(localDateTime));
Podemos ver que, após recuperarDateTimeFormatter, tudo o que precisamos fazer é chamar o métodoformat().
Para uma melhor compreensão, devemos nos familiarizar com possíveis cartas-padrão.
Vejamos as letras, por exemplo:
Symbol Meaning Presentation Examples
------ ------- ------------ -------
y year-of-era year 2004; 04
M/L month-of-year number/text 7; 07; Jul; July; J
d day-of-month number 10
H hour-of-day (0-23) number 0
m minute-of-hour number 30
s second-of-minute number 55
S fraction-of-second fraction 978
Todas as letras de padrão possíveis com explicação podem ser encontradas emhttps://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html.It’s worth to know that final value depends on the number of symbols. Há 'MMMM' no exemplo que imprime o nome completo do mês, enquanto uma única letra 'M' forneceria o número do mês sem um 0 inicial.
Para terminar emDateTimeFormatter, vamos ver como podemos formatarLocalizedDateTime:
LocalDateTime localDateTime = LocalDateTime.of(2018, 1, 1, 10, 15, 50, 500);
ZoneId losAngelesTimeZone = TimeZone.getTimeZone("America/Los_Angeles").toZoneId();
DateTimeFormatter localizedTimeFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.FULL);
String formattedLocalizedTime = localizedTimeFormatter.format(
ZonedDateTime.of(localDateTime, losAngelesTimeZone));
assertEquals("Monday, January 1, 2018 10:15:50 AM PST", formattedLocalizedTime);
Para formatarLocalizedDateTime, podemos usar o métodoofLocalizedDateTime(FormatStyle dateTimeStyle) e fornecer umFormatStyle. predefinido
Para uma análise mais aprofundada da API Java 8Date/Time, temos um artigo existentehere.
4.2. DateFormat eSimpleDateFormatter
Como ainda é comum trabalhar em projetos que usamDateseCalendars, apresentaremos brevemente os recursos de formatação de datas e horas com as classesDateFormateSimpleDateFormat.
Vamos analisar as habilidades do primeiro:
GregorianCalendar gregorianCalendar = new GregorianCalendar(2018, 1, 1, 10, 15, 20);
Date date = gregorianCalendar.getTime();
DateFormat ffInstance = DateFormat.getDateTimeInstance(
DateFormat.FULL, DateFormat.FULL, Locale.ITALY);
DateFormat smInstance = DateFormat.getDateTimeInstance(
DateFormat.SHORT, DateFormat.MEDIUM, Locale.ITALY);
assertEquals("giovedì 1 febbraio 2018 10.15.20 CET", ffInstance.format(date));
assertEquals("01/02/18 10.15.20", smInstance.format(date));
DateFormat funciona comDatese tem três métodos úteis:
-
getDateTimeInstance
-
getDateInstance
-
getTimeInstance
Todos eles usam valores predefinidos deDateFormat como parâmetro. Cada método está sobrecarregado, portanto, passarLocale também é possível. Se quisermos usar um padrão personalizado, como é feito emDateTimeFormatter, podemos usarSimpleDateFormat. Vamos ver um pequeno snippet de código:
GregorianCalendar gregorianCalendar = new GregorianCalendar(
2018, 1, 1, 10, 15, 20);
Date date = gregorianCalendar.getTime();
Locale.setDefault(new Locale("pl", "PL"));
SimpleDateFormat fullMonthDateFormat = new SimpleDateFormat(
"dd-MMMM-yyyy HH:mm:ss:SSS");
SimpleDateFormat shortMonthsimpleDateFormat = new SimpleDateFormat(
"dd-MM-yyyy HH:mm:ss:SSS");
assertEquals(
"01-lutego-2018 10:15:20:000", fullMonthDateFormat.format(date));
assertEquals(
"01-02-2018 10:15:20:000" , shortMonthsimpleDateFormat.format(date));
5. Costumização
Devido a algumas boas decisões de design, não estamos vinculados a um padrão de formatação específico da localidade e podemos configurar quase todos os detalhes para ficarmos totalmente satisfeitos com uma saída.
Para personalizar a formatação de número, podemos usarDecimalFormateDecimalFormatSymbols.
Vamos considerar um pequeno exemplo:
Locale.setDefault(Locale.FRANCE);
BigDecimal number = new BigDecimal(102_300.456d);
DecimalFormat zeroDecimalFormat = new DecimalFormat("000000000.0000");
DecimalFormat dollarDecimalFormat = new DecimalFormat("$###,###.##");
assertEquals(zeroDecimalFormat.format(number), "000102300,4560");
assertEquals(dollarDecimalFormat.format(number), "$102 300,46");
The DecimalFormat documentation mostra todos os caracteres de padrão possíveis. Tudo o que precisamos saber agora é que "000000000.000" determina zeros à esquerda ou à direita, ‘, 'é um separador de milhar e‘.' é um decimal.
Também é possível adicionar um símbolo de moeda. Podemos ver abaixo que o mesmo resultado pode ser alcançado usando a classeDateFormatSymbol:
Locale.setDefault(Locale.FRANCE);
BigDecimal number = new BigDecimal(102_300.456d);
DecimalFormatSymbols decimalFormatSymbols = DecimalFormatSymbols.getInstance();
decimalFormatSymbols.setGroupingSeparator('^');
decimalFormatSymbols.setDecimalSeparator('@');
DecimalFormat separatorsDecimalFormat = new DecimalFormat("$###,###.##");
separatorsDecimalFormat.setGroupingSize(4);
separatorsDecimalFormat.setCurrency(Currency.getInstance(Locale.JAPAN));
separatorsDecimalFormat.setDecimalFormatSymbols(decimalFormatSymbols);
assertEquals(separatorsDecimalFormat.format(number), "$10^[email protected]");
Como podemos ver, a classeDecimalFormatSymbols nos permite especificar qualquer formatação de número que possamos imaginar.
To customize SimpleDataFormat, we can use DateFormatSymbols.
Vamos ver como é simples mudar os nomes dos dias:
Date date = new GregorianCalendar(2018, 1, 1, 10, 15, 20).getTime();
Locale.setDefault(new Locale("pl", "PL"));
DateFormatSymbols dateFormatSymbols = new DateFormatSymbols();
dateFormatSymbols.setWeekdays(new String[]{"A", "B", "C", "D", "E", "F", "G", "H"});
SimpleDateFormat newDaysDateFormat = new SimpleDateFormat(
"EEEE-MMMM-yyyy HH:mm:ss:SSS", dateFormatSymbols);
assertEquals("F-lutego-2018 10:15:20:000", newDaysDateFormat.format(date));
6. Pacotes de Recursos
Finalmente, a parte crucial da internacionalização noJVM é o mecanismoResource Bundle.
O objetivo de umResourceBundle é fornecer um aplicativo com mensagens / descrições localizadas que podem ser externalizadas em arquivos separados. Cobrimos o uso e a configuração do Resource Bundle em um de nossos artigos anteriores -guide to the Resource Bundle.
7. Conclusão
Locales lixar os formatadores que os utilizam são ferramentas que nos ajudam a criar uma aplicação internacionalizada. Essas ferramentas nos permitem criar um aplicativo que pode se adaptar dinamicamente às configurações linguísticas ou culturais do usuário sem múltiplas compilações ou mesmo a necessidade de se preocupar se o Java suportaLocale.
Em um mundo em que um usuário pode estar em qualquer lugar e falar qualquer idioma, a capacidade de aplicar essas alterações significa que nossos aplicativos podem ser mais intuitivos e compreensíveis para mais usuários em todo o mundo.
Ao trabalhar com aplicativos Spring Boot, também temos um artigo conveniente paraSpring Boot Internationalization.
O código-fonte deste tutorial, com exemplos completos, pode ser encontradoover on GitHub.