Как начинающему разработчику, найти быстрый и надежный способ предложить свое приложение миру может быть непросто. Именно по этой причине вам необходимо изучить Java i18n и l10n (интернационализация и локализация).
Используя Java, у вас уже есть инструменты для определения локали пользователя и соответствующего перевода вашего приложения.
Интернационализация Java — важная часть разработки
Интернационализация происходит на этапе разработки, и чем раньше она будет реализована, тем меньше изменений в коде потребуется. Однако существует несколько мифов об i18n, которые мы хотели бы развенчать:
- Интернализация заключается только в переводе текста.
- Реализовать ее сложно, а сам процесс обременителен.
- Об этом должен думать только архитектор проекта.
Как вы уже догадались, все это ложные утверждения. Хотя локализация относительно проста в кодировании даже для начинающего разработчика, многие пакеты и библиотеки предоставляют способы перевести текст из вашего приложения и сделать его культурно корректным — это может включать форматы дат, стандарты измерения или даже весь дизайн пользовательского интерфейса.
Но так ли необходимо создавать новую структуру для вашего приложения? Никому не нравится возвращаться к своему коду и переписывать все заново. Agile-подход к разработке программного обеспечения учит нас внедрять все шаг за шагом, поэтому лучший сценарий — построить свой проект с учетом интернализации.
Давайте представим процесс следующим образом:
- Вы создаете приложение.
- Приложение считывает локаль пользователя и форматирует, даты и числа.
- Затем оно берет текстовые данные из заранее определенного ресурса и выводит правильный язык пользователю нашего приложения.
Поэтому должны быть способы хранить все важные сообщения в одном месте — вы можете переводить их самостоятельно или автоматизировать обновление контента с помощью такого удобного сервиса, как Crowdin.
С помощью Crowdin вы можете локализовать любые ветки git, какие захотите, или использовать CLI/API-интеграции. Читайте больше лайфхаков по переводу приложений.
Как реализовать интернализацию в Java
Java — мощный язык программирования, который предоставляет удобные способы работы с мультиязычными ресурсами. Лучший способ сделать это — так называемый ResourceBundle.
Класс ResourceBundle
ResourceBundle, вместе с Locale, являются основой интернализации в Java. Класс ResourceBundle используется для чтения строк из текстовых файлов (.properties).
Представьте, что вы можете хранить все ваши строки в одном файле, а переводчики сделают все остальное. Это и есть работа файла .properties. Каждый отдельный файл .properties обслуживает строки для каждой локали, которую поддерживает ваше приложение.
Свойства обычно имеют стандартное именование:
<ResouceBundleName>_<language_code>_<country_code>
например, MessageBundle_uk_UA для нового ResouceBundle.
MessageBundle.properties — это файл исходного языка.
А строки внутри него организованы следующим образом:
menu.addCity = Add a new visited city.
menu.chooseAction = Choose an action:
menu.removeCity = Remove city from the tracker.
menu.changeLocale = Change the localization of the app.
menu.exit = Exit the app.
menu.editCity = Edit an existing city.
Мы должны разделить все строки на категории и четко определить их имена. Когда мы хотим получить строку — справа — в нашем Java-приложении, мы вызываем ее, используя ее имя — слева.
Получение списка локалей в Java
Вы можете задаться вопросом: «Я уже создал пучок ресурсов, могу ли я назначить его локали?». — Да! Именно здесь мы используем класс Locale.
Он предоставляет способы взаимодействия с пользовательской системой, например, с помощью этих методов:
-> returns user’s default locale.
Using the system’s default localization on the app’s startup is a good practice. This method provides the user’s default locale, which you can use to load strings from ResourceBundle without the user even thinking about it.
```(String language_tag)```
-> returns Locale for the language.
As you have already noticed, each Resource Bundle can be created for specific language tags. Sometimes you may need to differentiate between Australian English (en_AU) and American English (en_US) - so you can specify it using this method.
```(Locale.getAvailableLocales()```
-> returns an array of all installed locales.
We use this method when we want to know which locales are installed on the user’s JVM. You parse the list - you know what to serve to the user.
### Dates and Numbers
As I mentioned earlier, different countries may have different ways of presenting Dates and Numbers. Let's assume we have 1 million. In English, it should be formatted as 1,000,000; however, in German, it should be 1.000.000.
To format numbers, Locale-aware creates an instance of NumberFormat class:
NumberFormat nf_en = NumberFormat.getInstance(Locale.ENGLISH);
String number_en = nf_en.format(1000000);
-> 1,000,000
NumberFormat nf_ge = NumberFormat.getInstance(Locale.GERMAN);
String number_ge = nf_ge.format(1000000);
-> 1.000.000
Same thing for dates. To format a date in Java, all you need to do is create an instance of DateFormat class:
DateFormat df_en = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.ENGLISH);
String date_en = df_en.format(new Date());
-> Aug 7, 2022
DateFormat df_ge = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.GERMAN);
String date_ge = df_ge.format(new Date());
-> 07.08.2022
The best part is instead of hard-coding the locale, we can use the user’s locale to format everything according to their culture!
## Context
So, you build the app and have all strings in English. Here’s the time you invite translators to your project. A lot of different people will look at the strings you wrote. How to build our resources so they will be translatable?
The concept of context plays a huge role in localization. Without it, some text may be translated poorly, but not because of the translator. Complex resources must have context attached!
If you name a string “string1,” - the translator will have no idea what this string’s for. Here’s a <a href="https://blog.crowdin.com/2019/11/05/no-context-no-quality-give-translators-context-to-get-better-translations/" target="_blank">little reading material</a>
on why it’s crucial.
Long story short: be sure to correctly name all your strings in the .properties files so that people can know the context of it.
## Java i18n Application Example
We have talked about internalization only in theory. Let’s get to the point with our tutorial, where we will use all our new knowledge.
This tutorial will help to create a light Java project and implement internalization. All the project files will be available to fork <a href="https://github.com/danielsutts/i18n-with-Crowdin" target="_blank">here</a>.
### App Overview:
Our application is a small city visit tracker. I’ll call it simple - City Visit Tracker. The user enters their name and is presented with a few actions:
1. Add a new city to the list.
2. Remove a city from the list.
3. Edit an already added city (we all had this problem remembering the dates, didn’t we?).
4. Change the localization of the app.
5. And, finally, exit the app.
I will use <a href="https://crowdin.com/on-demand-demo" target="_blank">Crowdin</a> for the translation of my resources. Using Crowdin - later in the tutorial.
First things first, let’s define the Client class.
```javascript
public class Client {
private Locale userLocale;
private final String username;
private Map<City, Date> visitedCities;
public Client(Locale userLocale, String username) {
this.userLocale = userLocale;
this.username = username;
}
public Locale getUserLocale() {
return userLocale;
}
public void setUserLocale(Locale userLocale) {
this.userLocale = userLocale;
}
public String getUsername() {
return username;
}
public void addCity(City city, Date dateVisited) {
this.visitedCities.put(city, dateVisited);
}
public void removeCity(City city, Date dateVisited) {
this.visitedCities.remove(city, dateVisited);
}
}
Мы хотим, чтобы наш класс пользователя хранил все посещенные города, имя пользователя и, самое главное, предпочитаемую локаль пользователя. Я также добавил геттеры и сеттеры, чтобы мы могли изменять информацию в соответствии с нашими потребностями.
Далее давайте определим класс City.
public class City {
private final Locale cityLocale;
private final String cityName;
private final String cityCountry;
public City(Locale cityLocale, String cityName, String cityCountry) {
this.cityLocale = cityLocale;
this.cityName = cityName;
this.cityCountry = cityCountry;
}
}
Поскольку мы позволяем пользователю добавлять свои города в список, давайте добавим все свойства cityLocale в класс, чтобы мы могли даже добавить функцию перевода названий городов в будущем, если нам это понадобится.
Ресурсы
Давайте создадим новую папку с именем Message Bundle и добавим несколько файлов .properties. Я хочу, чтобы в моем приложении были английский, польский и украинский языки. Поэтому я добавлю в папку четыре файла (четвертый — по умолчанию).
MessageBundle/
/MessageBundle.properties
/MessageBundle_en_US.properties
/MessageBundle_pl_PL.properties
/MessageBundle_uk_UA.properties
Для простоты мы не будем использовать файл по умолчанию, и я начну заполнять ресурсы в пучке en_US.
Итак, что нам нужно в нашем пучке ресурсов?
Нам нужно несколько сообщений меню:
menu.printAll = Print all cities.
menu.addCity = Add a new visited city.
menu.chooseAction = Choose an action:
menu.removeCity = Remove city from the tracker.
menu.changeLocale = Change the localization of the app.
menu.exit = Exit the app.
menu.editCity = Edit an existing city.
Нам нужно несколько основных слов, которые мы можем использовать:
word.yes = Yes
word.no = No
word.city = City
word.date = Date
And here are all the messages we are going to use in our application:
message.welcome = Welcome to City Visit Tracker!
message.hello = Hello,
message.enterYourName = Enter your name:
message.addCity = Add a new city!
message.addCityName = Enter the city name:
message.addCityDate = Enter the city visited date
message.saved = Successfully saved a new city!
message.cityDeleted = Successfully deleted a city!
message.cityEdited = Successfully edited a city!
message.forExample = for example:
message.noChanges = No changes will be applied.
message.chooseLocale = Choose one of available languages:
message.tryAgain = Try again.
message.noCities = No cities saved yet!
message.chooseDelete = Choose a city to delete.
message.chooseEdit = Choose a city to edit.
message.newCityName = Enter a new city name
message.newCityDate = Enter a new city visited date
Краудин и ресурсы
Я добавил все необходимые нам сообщения. Однако при разработке приложения хочется, чтобы оно было Agile. Интеграция Crowdin с Github доступна. Я пройдусь по интеграции.
- Зайдите в свой аккаунт Crowdin и авторизуйтесь.
- Откройте новый проект.
- Введите информацию о вашем проекте.
- Выберите исходный язык и целевой язык (в нашем случае исходным будет английский, а целевыми — украинский и польский.
- Выберите «Выбрать интеграцию» в главном меню -> GitHub Personal -> Авторизуйтесь на GitHub.
- Выберите репозиторий, с которым вы хотите работать, и выберите ветку.
- Вы хотите создать файл конфигурации в вашем репозитории, в котором нужно указать папку /src/resources для исходных файлов.
- Вы также хотите указать папку переведенных файлов. Таким образом, ваши переводчики смогут выдавать свою работу по частям, не прикасаясь к Git’у. Разве это не круто?
Следующий шаг — нанять профессионалов для перевода вашего текста. Но еще одна замечательная функция Crowdin позволяет нам переводить исходные файлы вручную! Давайте сделаем именно это.
- Перейдите в раздел «Главная» — выберите язык перевода — нажмите «Перевести». Теперь вы должны увидеть пользовательский интерфейс Crowdin, который позволяет выбрать наиболее подходящий перевод строк в нашем исходном файле.
- Пройдитесь по ним и выберите предложения ниже (кстати, они довольно хороши!).
- Когда вы дойдете до конца обоих файлов — украинского и польского, вернитесь назад и нажмите Proofread, чтобы утвердить переводы. (Обычно это задача директора по локализации, так что не бойтесь делать это сейчас — это только для практики 🙂
- В папке Home нажмите «Build & Download», чтобы загрузить переводы, которые вы только что создали. Теперь вы можете скопировать их в свою папку и использовать для интернационализации вашего приложения!
Продолжим работу с нашим приложением
Мы хотим, чтобы наше приложение могло записывать, редактировать и удалять города. Мы также хотим, чтобы оно выводило список всех городов, которые мы уже добавили.
Используя Java-классы DateFormat, ResourceBundle и Locale, давайте добавим эти методы в наш главный класс:
Чтобы изменить локаль приложения, мы просто распечатаем список локалей пользователя и попробуем загрузить его из нашего ResourceBundle.
private static void changeLocale() { System.out.println(userResourceBundle.getString("message.chooseLocale"));
System.out.println(Arrays.toString(locales));
Scanner scanner = new Scanner(System.in);
String newLocale = scanner.nextLine();
try {
userResourceBundle = ResourceBundle.getBundle("resources/MessageBundle", new Locale(newLocale));
} catch (MissingResourceException e) {
System.out.println(userResourceBundle.getString("message.tryAgain"));
changeLocale();
}
}
Далее мы предоставляем пользователю меню и ждем ввода. Здесь вы видите только несколько методов. Посетите страницу приложения на GitHub, чтобы узнать больше.
private static void menu() {
Scanner scanner = new Scanner(System.in);
// Print out the menu
System.out.println(userResourceBundle.getString("menu.chooseAction"));
System.out.println("0 - " + userResourceBundle.getString("menu.printAll"));
System.out.println("1 - " + userResourceBundle.getString("menu.addCity"));
System.out.println("2 - " + userResourceBundle.getString("menu.removeCity"));
System.out.println("3 - " + userResourceBundle.getString("menu.editCity"));
System.out.println("4 - " + userResourceBundle.getString("menu.changeLocale"));
System.out.println("5 - " + userResourceBundle.getString("menu.exit"));
String answer = scanner.nextLine();
…
// If Menu Action is "Add City"
if (answer.equals("1")) {
try{
addCity();
} catch (Exception ex) {
System.out.println(userResourceBundle.getString("message.noChanges")); System.out.println(userResourceBundle.getString("message.tryAgain"));
}
menu();
}
…
Здесь мы определяем метод addCity(), который добавляет город в репозиторий нашего пользователя. Как вы можете видеть здесь, мы используем все возможности ResourceBundle, DateFormats и Locales, чтобы предоставить пользователю наиболее аутентичный и интернационализированный опыт.
private static void addCity() throws ParseException {
// Take user's input
// Create a new City instance to fill it in
Scanner scanner = new Scanner(System.in);
City newCity = new City();
// Add the user's locale to the City instance
newCity.setCityLocale(userResourceBundle.getLocale());
// Print out the city name prompt and set the new name
System.out.println(userResourceBundle.getString("message.addCity"));
System.out.print(userResourceBundle.getString("message.addCityName"));
newCity.setCityName(scanner.nextLine());
// Print out the date prompt and set the new date
System.out.print(userResourceBundle.getString("message.addCityDate") + ' ');
// Here, we transform the input data according to the user's locale
// and save it as usual Date class
DateFormat df = DateFormat.getDateInstance(DateFormat.DEFAULT, userResourceBundle.getLocale());
System.out.println('(' + userResourceBundle.getString("message.forExample") + ' ' + pdf.format(new Date()) + ')');
Date newDate = df.parse(scanner.nextLine());
newCity.setVisitedDate(newDate);
repo.addCity(newCity);
// Tell user we saved a new city!
System.out.println(userResourceBundle.getString("message.saved"));
}
Давайте определим нашу вершину — основной метод City Visit Tracker!
Нам придется определить три статические переменные в файле CityApplication.java:
protected static ResourceBundle userResourceBundle;
private static final String[] locales = new String[]{"en_US", "pl_PL", "uk_UA"};
private static final CityRepository repo = new CityRepository();
ResourceBundle помогает нам переключаться между различными пучками l10n. Locales сообщает нам, какие языки поддерживаются нашим приложением. Repo инициализируется для хранения городов.
Теперь все, что мне нужно сделать при запуске приложения, это получить локаль пользователя по умолчанию, вывести несколько приветственных сообщений и запустить метод menu().
public static void main(String[] args) {
// Get user's default locale
// Load ResourceBundle for the locale
Locale locale = Locale.getDefault();
userResourceBundle = ResourceBundle.getBundle("resources/MessageBundle", locale);
Scanner scanner = new Scanner(System.in);
System.out.println(userResourceBundle.getString("message.welcome"));
System.out.print(userResourceBundle.getString("message.enterYourName") + ' ');
String name = scanner.nextLine();
Client client = new Client(locale, name);
System.out.println(userResourceBundle.getString("message.hello") + ' ' + client.getUsername() + "!");
menu();
}
И мы закончили. Теперь ваша очередь! Это приложение — небольшой пример того, что можно сделать с помощью связок Java i18n и l10n.
Заключение
В этом руководстве мы узнали, что предлагает нам Java в плане локализации и интернационализации наших приложений. I18n и l10n чрезвычайно важны, когда вы хотите предоставить пользователю абсолютно лучший опыт.
Класс ResourceBundle предоставляет нам простой способ работы с ресурсами.
Класс Locale предоставляет нам способы взаимодействия с локалью пользователя.
DateFormat и NumberFormat позволяют нам форматировать даты и числа в соответствии с локалью пользователя.
Не забывайте делать свои переводы простыми и чистыми. Создайте пакет по умолчанию и предоставьте работу переводчикам и корректорам. Мы также рассмотрели способ автоматизации процесса перевода с помощью Crowdin — как видите, это просто и быстро.