В ходе разработки одного из наших приложений нам понадобилось сделать поддержку мультиязычности. Задача была дать пользователю возможность менять язык (русский и английский) интерфейса приложения. При этом текста и контент должны переводиться «на лету».
Для этого нам нужно было решить 2 задачи:
В этой статья попробую подробно расписать как мы решили данные задачи. И так поехали.
Для определения текущего языка можно, конечно, воспользоваться библиотекой react-native-i18n, но мы решили обойтись без него, так как для решения этой задачи можно и без сторонних библиотек. Для этого пишем следующее:
Для ios мы извлекаем язык приложения через SettingsManager, а для android через нативный I18nManager.
Теперь когда мы получили текущий язык устройства, мы можем сохранить его в AsyncStorage и приступить ко второй задаче.
Для управления глобальным состоянием мы используем MobX, но вы можете использовать другое решение.
И так мы должны создать класс(мне нравится называть «модель»), который будет отвечать за глобальное состояния текущей локализации. Создаем:
При инициализации нашей модели мы вызываем метод init, который берет локаль либо из AsyncStorage, если там есть, либо извлекаем текущий язык устройства и кладет в AsyncStorage.
Далее нам нужно написать метод(action), который будет менять язык:
Думаю, что тут все понятно.
Теперь самое интересное. Сами переводы мы решили хранить простым словарем. Для этого создадим js файл рядом с нашей LangModel, в котором мы поместим наши переводы:
Далее реализуем еще один метод в LangModel, который будет принимать на вход текст и возвращать текст текущей локализации:
Все, наш LangModel готов.
Теперь мы можем использовать метод rk для локализация текста:
Посмотреть как это работает можно в нашем приложении в AppStore и Google Play (Нажать на иконку (!) справа вверху, пролистать вниз)
Конечно, писать каждый раз LangModel.rk это не круто. Поэтому мы можем создать собственный компонент Text и в нем уже использовать LangModel.rk
Так же нам может понадобиться, например, менять логотип приложения в зависимости от текущей локализации. Для этого можно просто менять контент в зависимости от LangModel.lang (не забудьте обернуть ваш компонент в observer(MobX))
P.S.: Возможно такой подход вам покажется не стандартным, но он нам понравился больше чем то, что предлагает react-native-i18n
На этом у меня все. Всем спасибо!)
Для этого нам нужно было решить 2 задачи:
- Определить текущий язык приложения.
- Использование глобального состояния для перевода «на лету».
В этой статья попробую подробно расписать как мы решили данные задачи. И так поехали.
Определяем текущий язык устройства
Для определения текущего языка можно, конечно, воспользоваться библиотекой react-native-i18n, но мы решили обойтись без него, так как для решения этой задачи можно и без сторонних библиотек. Для этого пишем следующее:
import {NativeModules, Platform} from 'react-native';
let deviceLanguage = (Platform.OS === 'ios'
? NativeModules.SettingsManager.settings.AppleLocale ||
NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13
: NativeModules.I18nManager.localeIdentifier
Для ios мы извлекаем язык приложения через SettingsManager, а для android через нативный I18nManager.
Теперь когда мы получили текущий язык устройства, мы можем сохранить его в AsyncStorage и приступить ко второй задаче.
Переводим «на лету»
Для управления глобальным состоянием мы используем MobX, но вы можете использовать другое решение.
И так мы должны создать класс(мне нравится называть «модель»), который будет отвечать за глобальное состояния текущей локализации. Создаем:
// ключ локального хранилища, в котором будем записывать текущий lang
const STORE = '@lang-store';
// список русскоязычных стран
const RU_LANGS = [
'ru',
'az',
'am',
'by',
'ge',
'kz',
'kg',
'md',
'tj',
'tm',
'uz',
'ua',
];
class LangModel {
@observable
lang = 'ru'; // по умолчанию
constructor() {
this.init();
}
@action
async init() {
const lang = await AsyncStorage.getItem(STORE);
if (lang) {
this.lang = lang;
} else {
let deviceLanguage: string = (Platform.OS === 'ios'
? NativeModules.SettingsManager.settings.AppleLocale ||
NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13
: NativeModules.I18nManager.localeIdentifier
).toLowerCase();
if (
RU_LANGS.findIndex((rulang) => deviceLanguage.includes(rulang)) === -1
) {
this.lang = 'en';
}
AsyncStorage.setItem(STORE, this.lang);
}
}
export default new LangModel();
При инициализации нашей модели мы вызываем метод init, который берет локаль либо из AsyncStorage, если там есть, либо извлекаем текущий язык устройства и кладет в AsyncStorage.
Далее нам нужно написать метод(action), который будет менять язык:
@action
changeLang(lang: string) {
this.lang = lang;
AsyncStorage.setItem(STORE, lang);
}
Думаю, что тут все понятно.
Теперь самое интересное. Сами переводы мы решили хранить простым словарем. Для этого создадим js файл рядом с нашей LangModel, в котором мы поместим наши переводы:
// translations.js
// Да, за основу мы взяли русский.
export default const translations = {
"Привет, Мир!": {en: "Hello, World!"},
}
Далее реализуем еще один метод в LangModel, который будет принимать на вход текст и возвращать текст текущей локализации:
import translations from './translations';
...
rk(text) {
if (!text) {
return text;
}
// если локаль ru, то переводить не нужно
if (this.lang === 'ru') {
return text;
}
// если перевода нет, кинем предупреждение
if (translations[text] === undefined || translations[text][this.lang] === undefined) {
console.warn(text);
return text;
}
return translations[text][this.lang];
}
Все, наш LangModel готов.
Полный код LangModel
import {NativeModules, Platform} from 'react-native';
import {observable, action} from 'mobx';
import AsyncStorage from '@react-native-community/async-storage';
import translations from './translations';
const STORE = '@lang-store';
// список ru локали
const RU_LANGS = [
'ru',
'az',
'am',
'by',
'ge',
'kz',
'kg',
'md',
'tj',
'tm',
'uz',
'ua',
];
class LangModel {
@observable
lang = 'en';
constructor() {
this.init();
}
@action
async init() {
// Берем текущую локаль из AsyncStorage
const lang = await AsyncStorage.getItem(STORE);
if (lang) {
this.lang = lang;
} else {
let deviceLanguage: string = (Platform.OS === 'ios'
? NativeModules.SettingsManager.settings.AppleLocale ||
NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13
: NativeModules.I18nManager.localeIdentifier
).toLowerCase();
if (
RU_LANGS.findIndex((rulang) => deviceLanguage.includes(rulang)) > -1
) {
this.lang = 'ru';
}
AsyncStorage.setItem(STORE, this.lang);
}
@action
changeLang(lang: string) {
this.lang = lang;
AsyncStorage.setItem(STORE, lang);
}
rk(text) {
if (!text) {
return text;
}
// если локаль ru, то переводить не нужно
if (this.lang === 'ru') {
return text;
}
// если перевода нет, кинем предупреждение
if (translations[text] === undefined || translations[text][this.lang] === undefined) {
console.warn(text);
return text;
}
return translations[text][this.lang];
}
}
export default new LangModel();
Теперь мы можем использовать метод rk для локализация текста:
<Text>{LangModel.rk("Привет, Мир!")}</Text>
Посмотреть как это работает можно в нашем приложении в AppStore и Google Play (Нажать на иконку (!) справа вверху, пролистать вниз)
Бонус
Конечно, писать каждый раз LangModel.rk это не круто. Поэтому мы можем создать собственный компонент Text и в нем уже использовать LangModel.rk
//components/text.js
import React from 'react';
import {Text} from 'react-native';
import {observer} from 'mobx-react';
import {LangModel} from 'models';
export const MyText = observer((props) => (
<Text {...props}>{props.notTranslate ? props.children : LangModel.rk(props.children)}</Text>
));
Так же нам может понадобиться, например, менять логотип приложения в зависимости от текущей локализации. Для этого можно просто менять контент в зависимости от LangModel.lang (не забудьте обернуть ваш компонент в observer(MobX))
P.S.: Возможно такой подход вам покажется не стандартным, но он нам понравился больше чем то, что предлагает react-native-i18n
На этом у меня все. Всем спасибо!)
defint
А какие конкретно проблемы в существующих библиотеках (react-18next, например) вы нашли? Почему решили написать собственную обвязку? Что будет, если переводимых сообщений будет 1000, держать в памяти все все переводы? Что делать с поддержкой множественных чисел? Если поменяется содержание ключа словаря — нужно поиском по проекту искать все вхождения некорректной надписи?
Saidai
Из плюсов не нужно придумывать названия ключей. Для простого приложения вполне подойдет, мне кажется.
defint
Но нужно ли для маленького приложения писать собственные решения, которые давно сделаны в нескольких интерпретациях? Придумать пару десятков ключей выглядит более простой задачей.
Но в любом случае автору виднее)
SteepP
Надо полагать что с ключами делают, потому что не додумались что можно просто текст перевести. Когда наступят на грабли, надо будет бегать и везде переделовать заново или городить залипухи, когда хочешь так, а когда хочешь эдак.
Ещё бы автоматический перевод прицепили от гугл транслейт, вообще бы класс. Не нашёл строки, переведи на лету.
Если хочется видеть значение ключа когда смотришь код, то лучше плагин в IDE добавить, который автоматически его показывает. И с придумаванием названий, тоже можно через плагин, ввести правило как будет ключ автоматически создаваться. Выделил текст, и по шорткату, автоматически создался ключ, добавились ресурсы для языков (можно и с гугл-транслейт) и текст заменился на код вызова i18. Мы такое делали, правда для оригинального VS.