[Angular, Разработка веб-сайтов] Ленивая подгрузка переводов с Angular
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Если вы когда-нибудь участвовали в разработке крупного angular-проекта с поддержкой локализации, то эта статья для вас. Если же нет, то возможно, вам будет интересно, как мы решили проблему скачивания больших файлов с переводами при старте приложения: в нашем случае ~2300 строк и ~200 Кб для каждого языка.
Немного контекста
Всем привет! Я Frontend-разработчик компании ISPsystem в команде VMmanager.
Итак, мы имеем крупный frontend-проект. Под капотом angular 9-й версии на момент написания статьи. Поддержка локализации осуществляется библиотекой ngx-translate. Сами переводы в проекте лежат в json-файлах. Для взаимодействия с переводчиками используется сервис POEditor.
Что не так с большими переводами?
Во-первых, необходимость скачивать большой json-файл при первом входе в приложение.
Да, этот файл можно закэшировать, но примерно каждые 2 недели выходит обновление продукта и после каждого обновления переводы должны скачиваться заново.
К тому же в проекте используется система разделения доступа, а это значит, что пользователь с минимальным уровнем доступа увидит только малую часть всех переводов (сложно сказать конкретно, но разница в несколько, а может и в десятки раз), а все остальное будет для него обузой.
Во-вторых, навигация в огромном json-файле просто неудобна.
Конечно, мы не пишем код в блокноте. Но все равно поиск определенного ключа в определенном namespace становится непростой задачей. Например, надо найти TITLE, который лежит внутри HOME(HOME.....TITLE), при условии что в файле есть еще сотня TITLE, а объект внутри HOME тоже содержит сотню ключей.
Что делать с этими проблемами?
Как можно догадаться из названия статьи: распилить переводы на кусочки, разложить по разным файлам и скачивать по мере их необходимости.
Эта концепция хорошо ложится на модульную архитектуру angular. Мы будем указывать в angular-модуле, какой кусок переводов нам для него нужен.
Еще нам может понадобиться один и тот же кусок переводов в разных частях (модулях) приложения, для этого можно его положить в отдельный файл. Конечно, можно положить этот кусок в главный файл, откуда он будет доступен во всем приложении, но вы не забыли, чем мы тут занимаемся? этот кусок опять же может быть частью исключительно администраторского интерфейса и менее привилегированным пользователям никогда не понадобится.
А еще часть переводов, которую можно положить отдельно, может быть частью других «отдельных» переводов (для более мелкого дробления на части).
На основании перечисленных хотелок получается примерно такая структура файлов:
<projectRoot>/i18n/
ru.json
en.json
HOME/
ru.json
en.json
HOME.COMMON/
ru.json
en.json
ADMIN/
ru.json
en.json
Тут файлы json в корне — это основные файлы, они будут скачиваться всегда (нужный, в зависимости от выбранного языка). Файлы в HOME — переводы необходимые только обычному пользователю. ADMIN — файлы необходимые только администратору. HOME.COMMON — файлы необходимые и пользователю, и администратору.
Каждый json-файл внутри должен иметь структуру, соответствующую его namespace:
- корневые файлы просто содержат {...};
- файлы внутри ADMIN содержат { "ADMIN": {...} };
- файлы внутри HOME.COMMON содержат { "HOME": { "COMMON": {...} } } ;
- и т.д.
Пока что это можно воспринимать как мою причуду, далее это будет обоснованно.
Будем отталкиваться именно от этой структуры. У вас она может быть немного другой, от этого поменяются некоторые части из представленного далее кода.
ngx-translate из коробки этого всего не умеет, но предоставляет достаточный функционал, чтобы это можно было реализовать своими силами:
- возможность обработать отсутствующий перевод — используем это, чтобы докачать необходимые переводы налету;
- возможность реализовать свой загрузчик файлов переводов — а это чтобы скачивать сразу несколько файлов переводов, необходимых текущему модулю.
Реализация
Скачиватель переводов: TranslateLoader
Чтобы сделать свой скачиватель переводов, необходимо создать класс реализующий один метод abstract getTranslation(lang: string): Observable<any>. Для семантики можно унаследовать его от абстрактного класса TranslateLoader (импортируется из ngx-translate), который мы далее будем использовать для провайдинга.
Так как наш класс будет не просто скачивать переводы, но и как-то должен их объединять в один объект, кода будет чуть больше, чем один метод:
export class MyTranslationLoader extends TranslateLoader implements OnDestroy {
/** Глобальный кэш с флагами скачанных файлов переводов (чтобы не качать их повторно, для разных модулей) */
private static TRANSLATES_LOADED: { [lang: string]: { [scope: string]: boolean } } = {};
/** Сортируем ключи по возрастанию длины (маленькие куски будут вмердживаться в большие) */
private sortedScopes = typeof this.scopes === 'string' ? [this.scopes] : this.scopes.slice().sort((a, b) => a.length - b.length);
private getURL(lang: string scope: string): string {
// эта строка будет зависеть от того, куда и как вы кладете файлы переводов
// в нашем случае они лежат в корне проекта в директории i18n
return `i18n/${scope ? scope + '/' : ''}${lang}.json`;
}
/** Скачиваем переводы и запоминаем, что мы их скачали */
private loadScope(lang: string, scope: string): Observable<object> {
return this.httpClient.get(this.getURL(lang, scope)).pipe(
tap(() => {
if (!MyTranslationLoader.TRANSLATES_LOADED[lang]) {
MyTranslationLoader.TRANSLATES_LOADED[lang] = {};
}
MyTranslationLoader.TRANSLATES_LOADED[lang][scope] = true;
})
);
}
/**
* Все скачанные переводы необходимо объединить в один объект
* т.к. мы знаем, что файлы переводов не имеют пересечений по ключам,
* можно вместо сложной логики глубокого мерджа просто наложить объекты друг на друга,
* но надо делать это в правильном порядке, именно для этого мы выше отсортировали наши scope по длине,
* чтобы наложить HOME.COMMON на HOME, а не наоборот
*/
private merge(scope: string, source: object, target: object): object {
// обрабатываем пустую строку для root модуля
if (!scope) {
return { ...target };
}
const parts = scope.split('.');
const scopeKey = parts.pop();
const result = { ...source };
// рекурсивно получаем ссылку на объект, в который необходимо добавить часть переводов
const sourceObj = parts.reduce(
(acc, key) => (acc[key] = typeof acc[key] === 'object' ? { ...acc[key] } : {}),
result
);
// также рекурсивно достаем нужную часть переводов и присваиваем
sourceObj[scopeKey] = parts.reduce((res, key) => res[key] || {}, target)?.[scopeKey] || {};
return result;
}
constructor(private httpClient: HttpClient, private scopes: string | string[]) {
super();
}
ngOnDestroy(): void {
// сбрасываем кэш, чтобы при hot reaload переводы перекачались
MyTranslationLoader.TRANSLATES_LOADED = {};
}
getTranslation(lang: string): Observable<object> {
// берем только еще не скачанные scope
const loadScopes = this.sortedScopes.filter(s => !MyTranslationLoader.TRANSLATES_LOADED?.[lang]?.[s]);
if (!loadScopes.length) {
return of({});
}
// скачиваем все и сливаем в один объект
return zip(...loadScopes.map(s => this.loadScope(lang, s))).pipe(
map(translates => translates.reduce((acc, t, i) => this.merge(loadScopes[i], acc, t), {}))
);
}
}
Как можно заметить, scope здесь используется и как часть url для скачивания файла, и как ключ для доступа к необходимой части json, именно поэтому директория и структура в файле должны совпадать.
Как это использовать, описано чуть дальше.
Докачиватель переводов: MissingTranslationHandler
Чтобы реализовать эту логику, необходимо сделать класс, имеющий метод handle. Проще всего унаследовать класс от MissingTranslationHandler, который импортируется из ngx-translate.
Описание метода в репозитории ngx-translate выглядит так:
export declare abstract class MissingTranslationHandler {
/**
* A function that handles missing translations.
*
* @param params context for resolving a missing translation
* @returns a value or an observable
* If it returns a value, then this value is used.
* If it return an observable, the value returned by this observable will be used (except if the method was "instant").
* If it doesn't return then the key will be used as a value
*/
abstract handle(params: MissingTranslationHandlerParams): any;
}
Нас интересует как раз второй вариант развития событий: вернуть Observable на скачивание нужного куска переводов.
export class MyMissingTranslationHandler extends MissingTranslationHandler {
// кэшируем Observable с переводом, т.к. при входе на страницу, для которой еще нет переводов,
// каждая translate pipe вызовет метод handle
private translatesLoading: { [lang: string]: Observable<object> } = {};
handle(params: MissingTranslationHandlerParams) {
const service = params.translateService;
const lang = service.currentLang || service.defaultLang;
if (!this.translatesLoading[lang]) {
// вызываем загрузку переводов через loader (тот самый, который реализован выше)
this.translatesLoading[lang] = service.currentLoader.getTranslation(lang).pipe(
// добавляем переводы в общее хранилище ngx-translate
// флаг true говорит о том, что объекты необходимо смерджить
tap(t => service.setTranslation(lang, t, true)),
map(() => service.translations[lang]),
shareReplay(1),
take(1)
);
}
return this.translatesLoading[lang].pipe(
// вытаскиваем необходимый перевод по ключу и вставляем в него параметры
map(t => service.parser.interpolate(service.parser.getValue(t, params.key), params.interpolateParams)),
// при ошибке эмулируем стандартное поведение, когда нет перевода — возвращаем ключ
catchError(() => of(params.key))
);
}
}
Мы в проекте всегда используем только строковые ключи (HOME.TITLE), но ngx-translate также поддерживает ключи в виде массива строк (['HOME', 'TITLE']). Если вы этим пользуетесь, то в обработке catchError необходимо добавить проверку вроде такой of(typeof params.key === 'string' ? params.key : params.key.join('.')).
Используем все вышеописанное
Чтобы использовать наши классы, необходимо указать их при импорте TranslateModule:
export function loaderFactory(scopes: string | string[]): (http: HttpClient) => TranslateLoader {
return (http: HttpClient) => new MyTranslationLoader(http, scopes);
}
// ...
// app.module.ts
TranslateModule.forRoot({
useDefaultLang: false,
loader: {
provide: TranslateLoader,
useFactory: loaderFactory(''),
deps: [HttpClient],
},
})
// home.module.ts
TranslateModule.forChild({
useDefaultLang: false,
extend: true,
loader: {
provide: TranslateLoader,
useFactory: loaderFactory(['HOME', 'HOME.COMMON']),
deps: [HttpClient],
},
missingTranslationHandler: {
provide: MissingTranslationHandler,
useClass: MyMissingTranslationHandler,
},
})
// admin.module.ts
TranslateModule.forChild({
useDefaultLang: false,
extend: true,
loader: {
provide: TranslateLoader,
useFactory: loaderFactory(['ADMIN', 'HOME.COMMON']),
deps: [HttpClient],
},
missingTranslationHandler: {/*...*/},
})
Флаг useDefaultLang: false необходим для корректной работы missingTranslationHandler.
Флаг extend: true (добавлен в версии ngx-translate@12.0.0) необходим, чтобы дочерние модули работали с переводами главного модуля.
Также, чтобы избавиться от копирования подобных импортов для каждого модуля с отдельными переводами, можно сделать пару утилит:
export function translateConfig(scopes: string | string[]): TranslateModuleConfig {
return {
useDefaultLang: false,
loader: {
provide: TranslateLoader,
useFactory: httpLoaderFactory(scopes),
deps: [HttpClient],
},
};
}
@NgModule()
export class MyTranslateModule {
static forRoot(scopes: string | string[] = [], config?: TranslateModuleConfig): ModuleWithProviders<TranslateModule> {
return TranslateModule.forRoot({
...translateConfig([''].concat(scopes)),
...config,
});
}
static forChild(scopes: string | string[], config?: TranslateModuleConfig): ModuleWithProviders<TranslateModule> {
return TranslateModule.forChild({
...translateConfig(scopes),
extend: true,
missingTranslationHandler: {
provide: MissingTranslationHandler,
useClass: MyMissingTranslationHandler,
},
...config,
});
}
}
Такие импорты должны быть только в корневых модулях отдельных частей приложения, далее (чтобы использовать translate пайпу или директиву) надо просто импортировать TranslateModule.
В данный момент (на версии ngx-translate@12.1.2) можно заметить, что при переключении языка, пока происходит скачивание переводов, пайпа translate выводит [object Object]. Это ошибка внутри самой пайпы.
POEditor
Как я упоминал ранее, мы выгружаем наши переводы в сервис POEditor, чтобы там с ними мог работать переводчик. Для этого в сервисе есть соответствующее API:
Оба этих хэндлера работают с полными файлами переводов, но у нас все переводы лежат в разных файлах. Значит, перед отправкой надо все переводы склеить в один файл, а при скачивании разложить по файлам.
Эту логику мы реализовали в python3 скрипте.
В общих чертах он использует тот же принцип объединения переводов, что и в MyTranslateLoader. Разделение происходит по той же схеме, только из большого файла, мы вычитаем куски.
В скрипте реализовано несколько команд:
- split — принимает на вход файл и директорию, в которой у вас подготовлена структура для переводов, и раскладывает переводы согласно этой структуре (в нашем примере — это директория i18n);
- join — делает обратное действие: принимает на вход путь до директории с переводами и кладет склеенный json либо в stdout, либо в указанный файл;
- download — скачивает переводы из POEditor, затем либо раскладывает их по файлам в переданной директории, либо кладет в один файл, переданный в аргументы;
- upload — соответственно загружает в POEditor переводы либо из переданной директории, либо из переданного файла;
- hash — считает md5 сумму всех переводов из переданной директории. Пригодится в том случае, если вы подмешиваете хеш в параметры для скачивания переводов, чтобы они не кэшировались в браузере при изменении.
Также там используется пакет argparse, который позволяет удобно работать с аргументами и генерирует --help команду.
Исходник скрипта в рамках статьи рассматривать не будем, потому что он достаточно большой, но вы можете найти его в репозитории по ссылке ниже.
Также в репозитории представлен демо проект, в котором реализовано все, что описано в статье. А также этот проект залит на платформу stackblitz, где можно все потыкать.
GitHub Репозиторий
Демо на Stackblitz
К чему мы пришли
Сейчас такой подход уже используется в VMmanager 6. Конечно, все наши переводы за один раз мы не стали разделять, потому что их достаточно много. Постепенно отделяем их от основного файла, а новый функционал стараемся реализовывать уже с разделением переводов.
На данный момент ленивую подгрузку переводов можно наблюдать в управлении пользователями панели, в настройках панели, а также в недавно реализованных резервных копиях.
А как вы решаете проблему больших файлов локализации? Или почему не стали этого делать?
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка веб-сайтов, CSS, HTML] Современные решения старых CSS-задач (1 часть): Удержание футера внизу страницы (перевод)
- [Angular, ReactJS, VueJS] Angular vs React vs Vue 2020
- [JavaScript, Программирование, Разработка веб-сайтов] Работа с файлами в JavaScript
- [JavaScript, ReactJS, Разработка веб-сайтов] Debouncing с помощью React Hooks: хук для функций
- [Веб-аналитика, Веб-дизайн, Медийная реклама, Разработка веб-сайтов] Игра не по правилам: опыт участия в известном российском конкурсе проектов
- [UML Design, Анализ и проектирование систем, Программирование, Разработка веб-сайтов] UML для самых маленьких: диаграмма классов
- [Разработка веб-сайтов, Разработка мобильных приложений] Lamptest.ru: 5 лет, 3500 ламп, новые возможности
- [Go, Python, Программирование, Разработка веб-сайтов] Священный холивар «Python vs Go»
- [Amazon Web Services, Angular, Облачные сервисы, Хостинг, Яндекс API] Как разместить статический сайт с помощью Yandex.Cloud Object Storage
- [Usability, Веб-дизайн, Интерфейсы, Разработка веб-сайтов, Разработка мобильных приложений] 21 метод UX-исследований: какой выбрать
Теги для поиска: #_angular, #_razrabotka_vebsajtov (Разработка веб-сайтов), #_angular, #_i18n, #_lazy_load, #_frontend, #_ispsystem, #_vmmanager, #_blog_kompanii_ispsystem (
Блог компании ISPsystem
), #_angular, #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:27
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Если вы когда-нибудь участвовали в разработке крупного angular-проекта с поддержкой локализации, то эта статья для вас. Если же нет, то возможно, вам будет интересно, как мы решили проблему скачивания больших файлов с переводами при старте приложения: в нашем случае ~2300 строк и ~200 Кб для каждого языка. Немного контекста Всем привет! Я Frontend-разработчик компании ISPsystem в команде VMmanager. Итак, мы имеем крупный frontend-проект. Под капотом angular 9-й версии на момент написания статьи. Поддержка локализации осуществляется библиотекой ngx-translate. Сами переводы в проекте лежат в json-файлах. Для взаимодействия с переводчиками используется сервис POEditor. Что не так с большими переводами? Во-первых, необходимость скачивать большой json-файл при первом входе в приложение. Да, этот файл можно закэшировать, но примерно каждые 2 недели выходит обновление продукта и после каждого обновления переводы должны скачиваться заново. К тому же в проекте используется система разделения доступа, а это значит, что пользователь с минимальным уровнем доступа увидит только малую часть всех переводов (сложно сказать конкретно, но разница в несколько, а может и в десятки раз), а все остальное будет для него обузой. Во-вторых, навигация в огромном json-файле просто неудобна. Конечно, мы не пишем код в блокноте. Но все равно поиск определенного ключа в определенном namespace становится непростой задачей. Например, надо найти TITLE, который лежит внутри HOME(HOME.....TITLE), при условии что в файле есть еще сотня TITLE, а объект внутри HOME тоже содержит сотню ключей. Что делать с этими проблемами? Как можно догадаться из названия статьи: распилить переводы на кусочки, разложить по разным файлам и скачивать по мере их необходимости. Эта концепция хорошо ложится на модульную архитектуру angular. Мы будем указывать в angular-модуле, какой кусок переводов нам для него нужен. Еще нам может понадобиться один и тот же кусок переводов в разных частях (модулях) приложения, для этого можно его положить в отдельный файл. Конечно, можно положить этот кусок в главный файл, откуда он будет доступен во всем приложении, но вы не забыли, чем мы тут занимаемся? этот кусок опять же может быть частью исключительно администраторского интерфейса и менее привилегированным пользователям никогда не понадобится. А еще часть переводов, которую можно положить отдельно, может быть частью других «отдельных» переводов (для более мелкого дробления на части). На основании перечисленных хотелок получается примерно такая структура файлов: <projectRoot>/i18n/
ru.json en.json HOME/ ru.json en.json HOME.COMMON/ ru.json en.json ADMIN/ ru.json en.json Тут файлы json в корне — это основные файлы, они будут скачиваться всегда (нужный, в зависимости от выбранного языка). Файлы в HOME — переводы необходимые только обычному пользователю. ADMIN — файлы необходимые только администратору. HOME.COMMON — файлы необходимые и пользователю, и администратору. Каждый json-файл внутри должен иметь структуру, соответствующую его namespace:
Пока что это можно воспринимать как мою причуду, далее это будет обоснованно. Будем отталкиваться именно от этой структуры. У вас она может быть немного другой, от этого поменяются некоторые части из представленного далее кода. ngx-translate из коробки этого всего не умеет, но предоставляет достаточный функционал, чтобы это можно было реализовать своими силами:
Реализация Скачиватель переводов: TranslateLoader Чтобы сделать свой скачиватель переводов, необходимо создать класс реализующий один метод abstract getTranslation(lang: string): Observable<any>. Для семантики можно унаследовать его от абстрактного класса TranslateLoader (импортируется из ngx-translate), который мы далее будем использовать для провайдинга. Так как наш класс будет не просто скачивать переводы, но и как-то должен их объединять в один объект, кода будет чуть больше, чем один метод: export class MyTranslationLoader extends TranslateLoader implements OnDestroy {
/** Глобальный кэш с флагами скачанных файлов переводов (чтобы не качать их повторно, для разных модулей) */ private static TRANSLATES_LOADED: { [lang: string]: { [scope: string]: boolean } } = {}; /** Сортируем ключи по возрастанию длины (маленькие куски будут вмердживаться в большие) */ private sortedScopes = typeof this.scopes === 'string' ? [this.scopes] : this.scopes.slice().sort((a, b) => a.length - b.length); private getURL(lang: string scope: string): string { // эта строка будет зависеть от того, куда и как вы кладете файлы переводов // в нашем случае они лежат в корне проекта в директории i18n return `i18n/${scope ? scope + '/' : ''}${lang}.json`; } /** Скачиваем переводы и запоминаем, что мы их скачали */ private loadScope(lang: string, scope: string): Observable<object> { return this.httpClient.get(this.getURL(lang, scope)).pipe( tap(() => { if (!MyTranslationLoader.TRANSLATES_LOADED[lang]) { MyTranslationLoader.TRANSLATES_LOADED[lang] = {}; } MyTranslationLoader.TRANSLATES_LOADED[lang][scope] = true; }) ); } /** * Все скачанные переводы необходимо объединить в один объект * т.к. мы знаем, что файлы переводов не имеют пересечений по ключам, * можно вместо сложной логики глубокого мерджа просто наложить объекты друг на друга, * но надо делать это в правильном порядке, именно для этого мы выше отсортировали наши scope по длине, * чтобы наложить HOME.COMMON на HOME, а не наоборот */ private merge(scope: string, source: object, target: object): object { // обрабатываем пустую строку для root модуля if (!scope) { return { ...target }; } const parts = scope.split('.'); const scopeKey = parts.pop(); const result = { ...source }; // рекурсивно получаем ссылку на объект, в который необходимо добавить часть переводов const sourceObj = parts.reduce( (acc, key) => (acc[key] = typeof acc[key] === 'object' ? { ...acc[key] } : {}), result ); // также рекурсивно достаем нужную часть переводов и присваиваем sourceObj[scopeKey] = parts.reduce((res, key) => res[key] || {}, target)?.[scopeKey] || {}; return result; } constructor(private httpClient: HttpClient, private scopes: string | string[]) { super(); } ngOnDestroy(): void { // сбрасываем кэш, чтобы при hot reaload переводы перекачались MyTranslationLoader.TRANSLATES_LOADED = {}; } getTranslation(lang: string): Observable<object> { // берем только еще не скачанные scope const loadScopes = this.sortedScopes.filter(s => !MyTranslationLoader.TRANSLATES_LOADED?.[lang]?.[s]); if (!loadScopes.length) { return of({}); } // скачиваем все и сливаем в один объект return zip(...loadScopes.map(s => this.loadScope(lang, s))).pipe( map(translates => translates.reduce((acc, t, i) => this.merge(loadScopes[i], acc, t), {})) ); } } Как можно заметить, scope здесь используется и как часть url для скачивания файла, и как ключ для доступа к необходимой части json, именно поэтому директория и структура в файле должны совпадать. Как это использовать, описано чуть дальше. Докачиватель переводов: MissingTranslationHandler Чтобы реализовать эту логику, необходимо сделать класс, имеющий метод handle. Проще всего унаследовать класс от MissingTranslationHandler, который импортируется из ngx-translate. Описание метода в репозитории ngx-translate выглядит так: export declare abstract class MissingTranslationHandler {
/** * A function that handles missing translations. * * @param params context for resolving a missing translation * @returns a value or an observable * If it returns a value, then this value is used. * If it return an observable, the value returned by this observable will be used (except if the method was "instant"). * If it doesn't return then the key will be used as a value */ abstract handle(params: MissingTranslationHandlerParams): any; } Нас интересует как раз второй вариант развития событий: вернуть Observable на скачивание нужного куска переводов. export class MyMissingTranslationHandler extends MissingTranslationHandler {
// кэшируем Observable с переводом, т.к. при входе на страницу, для которой еще нет переводов, // каждая translate pipe вызовет метод handle private translatesLoading: { [lang: string]: Observable<object> } = {}; handle(params: MissingTranslationHandlerParams) { const service = params.translateService; const lang = service.currentLang || service.defaultLang; if (!this.translatesLoading[lang]) { // вызываем загрузку переводов через loader (тот самый, который реализован выше) this.translatesLoading[lang] = service.currentLoader.getTranslation(lang).pipe( // добавляем переводы в общее хранилище ngx-translate // флаг true говорит о том, что объекты необходимо смерджить tap(t => service.setTranslation(lang, t, true)), map(() => service.translations[lang]), shareReplay(1), take(1) ); } return this.translatesLoading[lang].pipe( // вытаскиваем необходимый перевод по ключу и вставляем в него параметры map(t => service.parser.interpolate(service.parser.getValue(t, params.key), params.interpolateParams)), // при ошибке эмулируем стандартное поведение, когда нет перевода — возвращаем ключ catchError(() => of(params.key)) ); } } Мы в проекте всегда используем только строковые ключи (HOME.TITLE), но ngx-translate также поддерживает ключи в виде массива строк (['HOME', 'TITLE']). Если вы этим пользуетесь, то в обработке catchError необходимо добавить проверку вроде такой of(typeof params.key === 'string' ? params.key : params.key.join('.')). Используем все вышеописанное Чтобы использовать наши классы, необходимо указать их при импорте TranslateModule: export function loaderFactory(scopes: string | string[]): (http: HttpClient) => TranslateLoader {
return (http: HttpClient) => new MyTranslationLoader(http, scopes); } // ... // app.module.ts TranslateModule.forRoot({ useDefaultLang: false, loader: { provide: TranslateLoader, useFactory: loaderFactory(''), deps: [HttpClient], }, }) // home.module.ts TranslateModule.forChild({ useDefaultLang: false, extend: true, loader: { provide: TranslateLoader, useFactory: loaderFactory(['HOME', 'HOME.COMMON']), deps: [HttpClient], }, missingTranslationHandler: { provide: MissingTranslationHandler, useClass: MyMissingTranslationHandler, }, }) // admin.module.ts TranslateModule.forChild({ useDefaultLang: false, extend: true, loader: { provide: TranslateLoader, useFactory: loaderFactory(['ADMIN', 'HOME.COMMON']), deps: [HttpClient], }, missingTranslationHandler: {/*...*/}, }) Флаг useDefaultLang: false необходим для корректной работы missingTranslationHandler. Флаг extend: true (добавлен в версии ngx-translate@12.0.0) необходим, чтобы дочерние модули работали с переводами главного модуля. Также, чтобы избавиться от копирования подобных импортов для каждого модуля с отдельными переводами, можно сделать пару утилит: export function translateConfig(scopes: string | string[]): TranslateModuleConfig {
return { useDefaultLang: false, loader: { provide: TranslateLoader, useFactory: httpLoaderFactory(scopes), deps: [HttpClient], }, }; } @NgModule() export class MyTranslateModule { static forRoot(scopes: string | string[] = [], config?: TranslateModuleConfig): ModuleWithProviders<TranslateModule> { return TranslateModule.forRoot({ ...translateConfig([''].concat(scopes)), ...config, }); } static forChild(scopes: string | string[], config?: TranslateModuleConfig): ModuleWithProviders<TranslateModule> { return TranslateModule.forChild({ ...translateConfig(scopes), extend: true, missingTranslationHandler: { provide: MissingTranslationHandler, useClass: MyMissingTranslationHandler, }, ...config, }); } } Такие импорты должны быть только в корневых модулях отдельных частей приложения, далее (чтобы использовать translate пайпу или директиву) надо просто импортировать TranslateModule. В данный момент (на версии ngx-translate@12.1.2) можно заметить, что при переключении языка, пока происходит скачивание переводов, пайпа translate выводит [object Object]. Это ошибка внутри самой пайпы. POEditor Как я упоминал ранее, мы выгружаем наши переводы в сервис POEditor, чтобы там с ними мог работать переводчик. Для этого в сервисе есть соответствующее API: Оба этих хэндлера работают с полными файлами переводов, но у нас все переводы лежат в разных файлах. Значит, перед отправкой надо все переводы склеить в один файл, а при скачивании разложить по файлам. Эту логику мы реализовали в python3 скрипте. В общих чертах он использует тот же принцип объединения переводов, что и в MyTranslateLoader. Разделение происходит по той же схеме, только из большого файла, мы вычитаем куски. В скрипте реализовано несколько команд:
Также там используется пакет argparse, который позволяет удобно работать с аргументами и генерирует --help команду. Исходник скрипта в рамках статьи рассматривать не будем, потому что он достаточно большой, но вы можете найти его в репозитории по ссылке ниже. Также в репозитории представлен демо проект, в котором реализовано все, что описано в статье. А также этот проект залит на платформу stackblitz, где можно все потыкать. GitHub Репозиторий Демо на Stackblitz К чему мы пришли Сейчас такой подход уже используется в VMmanager 6. Конечно, все наши переводы за один раз мы не стали разделять, потому что их достаточно много. Постепенно отделяем их от основного файла, а новый функционал стараемся реализовывать уже с разделением переводов. На данный момент ленивую подгрузку переводов можно наблюдать в управлении пользователями панели, в настройках панели, а также в недавно реализованных резервных копиях. А как вы решаете проблему больших файлов локализации? Или почему не стали этого делать? =========== Источник: habr.com =========== Похожие новости:
Блог компании ISPsystem ), #_angular, #_razrabotka_vebsajtov ( Разработка веб-сайтов ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:27
Часовой пояс: UTC + 5