Создано: 20 марта 2022 9:58 PM
Опубликовано: Да
Недавно я получил задание реализовать локализацию для приложения Flutter, которое мы создавали в течение последних нескольких месяцев.
Немного предыстории: уже несколько лет у нас есть версия нашего приложения Progressive Web App (PWA), и мы решили сделать его нативным приложением, чтобы использовать больше преимуществ мобильных технологий, в основном в отношении хранения файлов (в PWA вы, по сути, используете хранилище браузера, которое ограничено 50% от всего дискового пространства, а каждый origin имеет только 20% от этого или максимум 2 ГБ).
PWA построено на React (JS duh) и локализовано для поддержки 4 языков. Был использован [i18n-js](https://www.npmjs.com/package/i18n-js)
npm для обеспечения перевода сообщений во время выполнения на основе указанной локали.
Локализованные строки предоставляются в виде JSON-структур с ключами в качестве идентификаторов перевода и значениями в качестве собственно переведенных строк.
//en.js
account: {
language: 'Language',
settings: {
label: 'Settings'
},
about: 'About',
consent: 'Consent',
support: {
title: 'Support Lines',
subtitle: 'You can contact us through these methods for further help:'
}
}
//fr.js
account: {
language: 'Langue',
settings: {
label: 'Réglages'
},
about: 'À propos',
consent: 'Consentement',
support: {
title: 'Assistance',
subtitle: 'Vous pouvez nous contacter comme qui suit si vous avez besoin d’aide:'
}
}
Строки затем вставляются во время выполнения с целевой локалью (fr
в данном случае)
<ListItemText
primary={I18n.t('account.language', { locale })}
/>
Это позволяет нам локализовать приложение для любого количества локалей, для которых мы можем создать переводы.
Мы планировали сохранить PWA, так как для некоторых пользователей браузер будет оставаться точкой доступа. Кроме того, мы не настроили развертывание iOS для родного приложения, поэтому оно будет доступно только для пользователей Android.
Наша цель состояла в том, чтобы использовать те же файлы перевода, что и в PWA для флаттера. Мы также не хотели вручную обновлять оба файла перевода, так как при изменении любого из них неизбежно возникали пропущенные переводы. Ключевым моментом здесь была поддерживаемость переводов как в PWA на Javascript, так и в приложении Flutter на dart.
Возникло несколько проблем:
- Flutter преобразует все ключи в геттеры для класса, созданного через генерацию кода. JSON позволяет строковые ключи, которые могут содержать числа и специальные символы, такие как дефис (-), все из которых являются недопустимыми символами для имен переменных во Flutter.
- Пакет
intl
, отвечающий за генерацию кода во flutter, требует, чтобы файл JSON был плоским, без вложенных структур. -
Заглушки для вставки динамических строк требуют другой структуры во Flutter, чем в PWA с пакетом
I18n
.JSON для пакета
I18n
для динамических строк имеет следующую структуру:numericalRangeFeedback: 'The correct answer is a number between {lowerNumber} and {higherNumber} inclusive',
Соответствующая структура JSON, подходящая для Flutters
intl
, показана ниже:"@question_numericalRangeFeedback": { "placeholders": { "higherNumber": { "type": "String" }, "lowerNumber": { "type": "String" } } }
Это означало, что нам пришлось проделать дополнительную работу по изменению файлов перевода для использования во Flutter, причем сделать это нужно было так, чтобы их было легко поддерживать. Здравствуйте, автоматизация,
Прежде всего мы решили создать скрипт для процесса сборки pwa-репо:
- Вставлять ключевые слова на соответствующих языках в структуру JSON
- Заменить все дефисы в ключах на символы подчеркивания, которые разрешены во Flutter
- Сгладить все ключи.
- Заменить скобки динамической строки на новую json-запись с ключом
placeholders
. - Загрузите обработанный перевод JSON в удаленное место. В данном случае используется Firebase.
В идеале это преобразование должно быть сделано в репозитории flutter, поскольку обработка структуры JSON в приемлемый для flutter формат — это больше забота репозитория flutter. Однако мы не стали сильно заморачиваться по этому поводу и решили, что проще использовать javascript и пакеты npm для создания скриптов перевода.
const dynamicRegex = /{(w+)}/gim;
const getDynamicStrings = (str) => {
return [...str.match(dynamicRegex)];
}
const removeBraces = (str) => {
return str.replace(/{|}/gmi, "");
}
const formatWithSingleBraces = (multiBracedText) => {
const leftRegex = /{+/gm;
const rightRegex = /}+/gm;
return multiBracedText.replace(leftRegex, '{').replace(rightRegex, '}')
}
const flattenObjectStructure = (originalObject) => {
return flatten(originalObject, {
delimiter: '_',
transformKey: function(key){
return key.replace(new RegExp('-'), "_")
}})
}
const addAnnotatedDynamicEntry = (translationObject, key) => {
let newEntry = {};
newEntry[`@${key}`] = {placeholders: {}};
const dynamicStrings = getDynamicStrings(translationObject[key]);
for (const dynamicString of dynamicStrings){
const dynamicKey = removeBraces(dynamicString);
newEntry[`@${key}`].placeholders[dynamicKey] = {
"type": "String"
}
}
return newEntry;
}
const processDynamicStrings = ( flatTranslation ) => {
const processedTranslation = {};
for(let key in flatTranslation){
processedTranslation[key] = formatWithSingleBraces(flatTranslation[key]);
if(dynamicRegex.test(flatTranslation[key])){
const newEntry = addAnnotatedDynamicEntry(flatTranslation, key)
Object.assign(processedTranslation, newEntry)
}
}
return processedTranslation;
}
for(let locale in availableLocales){
if(!(locale in translationMap)){
throw Error('Unsupported locale')
}
const translation = translationMap[locale](keywords[locale]);
const flattenedTranslation = flattenObjectStructure(translation);
const finalTranslation = processDynamicStrings(flattenedTranslation);
const docRef = db.collection('translations').doc(locale);
docRef.set(finalTranslation)
.then(() => console.info(`${locale} locale uploaded`))
.catch((err) => {
console.error(err)
});
}
Благодаря этому процессу при каждом изменении базовых JSON-файлов перевода и их фиксации в продакшен запускается новая сборка, файлы обрабатываются и загружаются в Firebase.
Дополнительно были добавлены тесты для проверки всех переводов на соответствие английским переводам, чтобы выявить недостающие переводы и отказать в случае их обнаружения.
В процессе сборки Flutter запускается файл dart для загрузки всех JSON из удаленного репозитория, очистки строк (удаление всех ненужных символов) и создания новых файлов .arb
(на основе JSON), которые intl
использует для генерации классов и геттеров для переводов. Прелесть создания intl классов и геттеров заключается в том, что если какой-либо из ранее использованных переводов будет ошибочно удален, статическая проверка типов flutters укажет нам на это.
Обратите внимание, что я не получаю данные напрямую из firebase, потому что настройка firebase на flutter оказалась не такой простой, как хотелось бы, и это оказалось большой работой — просто тянуть json-строки с удаленного сервера. Поэтому я создал прокси-сервер с помощью expressjs
, который выполняет всю работу, и настроил firebase на нем.
void main() {
getTranslations();
}
void getTranslations() async {
var requestBody = {
'translationKeys': jsonEncode(['en', 'rw', 'es', 'fr', 'sw'])
};
var url =
Uri.parse('remote server with translations url');
var response = await http.post(url,
headers: {'Content-Type': 'application/json'},
body: json.encode(requestBody));
var translations = jsonDecode(response.body);
translations.forEach((key, value) async {
var filename = 'lib/l10n/intl_$key.arb';
await File(filename)
.writeAsString(removeHTMLTags(jsonEncode(value)).replaceAll('%', ""));
});
}
Вот и всё, автоматизированный и удобный способ обеспечить согласованность файлов перевода в обоих репозиториях.
Более важной темой, представляющей интерес, является интернационализация и локализация ваших приложений для различных областей, которые вы обслуживаете или можете обслуживать. Если вы планируете расти, вы всегда должны помнить о том, что визуальные строки в вашем приложении должны быть легко заменяемы на различные локализованные версии, когда вы в конечном итоге займетесь интернационализацией. Конечно, существует также архитектура компонента вашего приложения для поддержки право-левых языков, таких как арабский и урду, но это требует немного большего, чем описано выше, и, возможно, будет рассмотрено в отдельной статье блога.
Вы можете прочитать больше об интернационализации в flutter здесь.