Bom dia
Está na hora de ngx-traduzir hacks de vida. Inicialmente, planejei três partes, mas como a segunda parte não é realmente muito informativa - tentarei resumir a segunda parte o mais breve possível.
Parte 1
Considere AppTranslateLoader
para substituir o TranslateHttpLoader
. Nosso AppTranslateLoader
, em primeiro AppTranslateLoader
prestará atenção ao idioma do navegador e conterá a lógica de fallback, importará as localizações do MomentJs e carregará através do APP_INITIALIZER. Além disso, como resultado da combinação de duas partes dos hacks, ao longo do caminho, vamos nos aprofundar na criação de uma infraestrutura de localização conveniente e flexível no projeto.
O objetivo principal não é AppTranslateLoader
(porque é bastante simples e não é difícil de fazer), mas a criação de infraestrutura.
Tentei escrever o mais acessível possível, mas como o artigo tem muitas coisas que podem ser descritas em mais detalhes - levará muito tempo e não será interessante para quem já sabe como. Portanto, o artigo não foi muito amigável para iniciantes. Por outro lado, no final, há um link para exemplo de prodg.
Antes de começar, quero observar que, além do download de idiomas via http, é possível escrever um carregador de forma que carregue os idiomas necessários em nosso pacote na fase de montagem. Portanto, você não precisa adicionar carregadores para http, mas, por outro lado, com essa abordagem, será necessário reconstruir o aplicativo toda vez que alterarmos nossos arquivos com localizações, além disso, isso poderá aumentar bastante o tamanho do pacote .js.
// webpack-translate-loader.ts import { TranslateLoader } from '@ngx-translate/core'; import { Observable } from 'rxjs/Observable'; export class WebpackTranslateLoader implements TranslateLoader { getTranslation(lang: string): Observable<any> { return Observable.fromPromise(System.import(`../assets/i18n/${lang}.json`)); } }
Se o IDE xingar no System
você precisará adicioná-lo a typings.d.ts:
declare var System: System; interface System { import(request: string): Promise<any>; }
Agora podemos usar o WebpackTranslateLoader
no app.module:
@NgModule({ bootstrap: [AppComponent], imports: [ TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: WebpackTranslateLoader } }) ] }) export class AppModule { }
AppTranslateLoader
Então, vamos começar a escrever nosso AppTranslateLoader
. Para começar, quero identificar vários problemas que terão que ser encontrados usando o TranslateHttpLoader
padrão:
Traduzir cintilando. TranslateHttpLoader
não sabe como executar como parte do processo de inicialização do aplicativo e podemos entrar em uma situação em que, após a inicialização, vemos que temos o local para os rótulos corretos no aplicativo - as chaves (MY_BUTTON_KEY é o local do botão My), que após um momento muda para o texto correto.
Datas Seria bom ter um serviço que alterna a localização de datas. Quando se trata de localizar texto, é provável que você precise cuidar da localização de datas, horários, etc. Você pode usar momentJs ou a solução i18n incorporada ao Angular. Ambas as soluções são boas e possuem tubos Angular 2+ para formatação de vistas.
- Armazenamento em cache. usando o
TranslateHttpLoader
, você deve configurar seu servidor FE para armazenar em cache corretamente seus pacotes configuráveis json. Caso contrário, os usuários verão versões antigas da localização, pior serão as chaves de localização (se novas foram adicionadas após o cache pelo usuário). Não quero me incomodar toda vez que implanto em um novo servidor com o momento de configurar o cache. Portanto, faremos com que o Webpack faça tudo por nós da maneira que faz nos pacotes .js.
Rascunho de AppTranslateLoader
Soluções para problemas:
1. traduza o problema de tremulação - use AppTranslateLoader
como parte de APP_INITIALIZER
APP_INITIALIZER também esteve envolvido ativamente em um artigo sobre token de atualização , se não tiver bom gosto sobre o inicializador - aconselho a ler o artigo, apesar do fato de ser sobre token de atualização. De fato, a decisão de usar o inicializador é muito óbvia (para quem tem um inicializador), mas ainda espero que haja pessoas que venham a calhar:
//app.module.ts export function translationLoader(loader: AppTranslateLoader) { return () => loader.loadTranslation(); } @NgModule({ bootstrap: [AppComponent], providers: [ { provide: APP_INITIALIZER, useFactory: translationLoader, deps: [AppTranslateLoader], multi: true } ] }) export class AppModule { }
2. O problema das datas. Vamos apenas mudar o idioma em momentosJs junto com ngx-trad.
Tudo é simples aqui - depois que o json com localização é carregado, apenas mudamos a localização para momentJs (ou i18n).
Também é importante notar que os momentJs, como o i18n, podem importar localizações separadamente, os momentJs também podem importar um pacote, mas o pacote inteiro de localizações leva ~ 260 KB e você precisa apenas de dois deles.
Nesse caso, você pode importar apenas dois deles diretamente no arquivo em que o AppTranslateLoader
declarado.
import 'moment/locale/en-gb'; import 'moment/locale/ru';
Agora, as localizações en-gb e ru estarão no pacote de aplicativos js. No AppTranslateLoader
você pode adicionar um manipulador de idioma carregado recentemente:
export Class AppTranslateLoader { // .... private onLangLoaded(newLang: string) { // if (this.loadedLang && this.loadedLang !== newLang) { this.translate.resetLang(this.loadedLang); } this.loadedLang = newLang; this.selectedLang = newLang; // TODO: // , // en ru, momentJs en. moment().locale(newLang); // . momentJs localStorage.setItem(this.storageKey, newLang); // ls this.loadSubj.complete(); // - . }
!!! este manipulador tem uma desvantagem: se em nosso projeto apenas a localização en for fornecida para ngx-translate, mas por exemplo, momento ou precisar usar en ou en-gb, a lógica do processador precisará ser expandida ou a localização en-gb também deverá ser fornecida em ngx-translate.
!!! no momento com // TODO: podemos escrever um plug-in para o webpack, consideraremos alguns plug-ins mais tarde, mas ainda não o tenho.
Você pergunta, por que é impossível carregar localizações de datas e horas, bem como localizações de texto na interface (dinamicamente, via HTTP)? Isso ocorre porque as localizações de data contêm sua própria lógica e, portanto, são apresentadas como código javascript .
Mas, apesar disso, existe uma maneira de carregar essas localizações escrevendo um pequeno código 'sujo'. Eu não uso esse código na produção, mas a segunda localização dentro do meu pacote não me incomoda. Mas se você possui muitas localizações, deseja carregá-las dinamicamente e de maneira não muito segura, lembre-se:
private async loadAngularCulture(locale) { let angularLocaleText = await this.httpClient.get(`assets/angular-locales/${locale}.js`).toPromise();
A última vez que testei esse método no Angular 4. Provavelmente ele está funcionando agora.
Infelizmente, esse truque de vida sujo não funcionará no caso de c momentJs (apenas localização angular). Pelo menos não consegui encontrar uma maneira de fazer isso, mas se você é um programador de hackers muito barbudo, ficarei feliz em ver a solução nos comentários.
3. Armazenamento em cache. Semelhante à criação de um pacote configurável .js, você pode adicionar um hash ao nome do pacote configurável .json.
Tudo depende de como exatamente você coleta todos os json'es em um arquivo, talvez você tenha apenas tudo em um arquivo. Na Internet, você pode encontrar vários módulos npm que podem coletar json pequeno em um arquivo. Não encontrei aqueles que podem anexar um hash e coletar tudo em um arquivo. O próprio Webpack também não pode manipular o json conforme exigido pelos detalhes do ngx-translate. Portanto, escreveremos nosso plugin webpack.
Em resumo: precisamos coletar todos os json no projeto de acordo com um padrão específico, enquanto precisamos agrupá-los por nome (en, ru, de, etc.), porque, por exemplo, en.json pode estar em pastas diferentes. Em seguida, para cada arquivo coletado, você precisa anexar um hash.
Há um problema aqui. Como o AppTranslateLoader
reconhece os nomes dos arquivos se cada localização tem seu próprio nome? Por exemplo, incluindo o pacote configurável em index.html, podemos incluir o HtmlWebpackPlugin e solicitar que ele adicione uma tag de script com o nome do pacote configurável por si só.
Para resolver esse problema nas localizações .json, nosso plugin webpack criará o config.json, que conterá a associação do código do idioma ao nome do arquivo hash:
{ "en": "en.some_hash.json", "ru": "ru.some_hash.json" }
O config.json também será armazenado em cache pelo navegador, mas levará um pouco e podemos simplesmente especificar um parâmetro queryString aleatório quando GET aumentar o tamanho desse arquivo (portanto, carregá-lo constantemente novamente). Ou atribua um ID aleatório ao config.json (descreverei esse método, o primeiro pode ser encontrado no Google).
Também quero simplificar um pouco a infraestrutura e a atomicidade das localizações. O json com localização estará na pasta com seu componente. E, para evitar chaves duplicadas, a estrutura do pacote json será construída com base no caminho para um arquivo json específico. Por exemplo, temos dois en.json, um localizado no caminho src/app/article-component
e o outro src/app/comment-component
. Eu quero obter o seguinte json na saída:
{ "article-component": { "TITLE": "Article title" }, "comment-component": { "TITLE": "Comment title" } }
Podemos descartar a parte do caminho que não precisamos, para que as chaves fiquem o mais curtas possível nas visualizações.
!!! Existe uma desvantagem: quando você coloca o componente em outra pasta, a chave de localização muda.
Posteriormente, consideraremos outro truque de vida que nos permitirá indicar no componente apenas o último campo-chave, independentemente de onde e quão profundo nosso projeto está no projeto, e, portanto, podemos transferi-lo como desejar e renomear como quiser.
Basicamente, eu quero alcançar o encapsulamento e até uma dica de polimorfismo das localizações de conversão de ngx. Gosto do conceito de encapsular vistas no encapsulamento Angular - Angular View , ou melhor, no DOM DOM . Sim, isso aumenta o tamanho do aplicativo como um todo, mas direi antecipadamente que depois que o ngx-translate se tornou mais encapsulado, trabalhar com arquivos de localização se tornou muito mais agradável. Os componentes começaram a se importar apenas com suas localizações. Além disso, será possível redefinir as localizações no componente filho, dependendo das localizações no componente pai. Além disso, agora você pode transferir componentes de um projeto para outro, e eles já estarão localizados. Mas como em outros lugares, há nuances, mais sobre isso mais tarde.
Então vamos ao nosso plugin. O que é e como . mesclar o plug-in de localização .
O código-fonte do carregador e do plug-in pode ser encontrado no link de exemplo na parte inferior do artigo (pasta ./build-utils).
O plug-in faz tudo com o que está escrito acima e aceita as seguintes opções:
- omitir. nomes no caminho para a localização que precisam ser ignorados (este é exatamente o momento em que desejo remover as partes extras do caminho para o arquivo)
- fileInput. regular para buscar arquivos de localização no prodge (como teste no webpack)
- rootDir. por onde começar a procurar arquivos pelo padrão fileInput
- outputDir. onde o arquivo de configuração e as localizações serão criados na pasta dist
- configName. sob qual nome o arquivo de configuração será criado.
No meu projeto, o plugin está conectado desta maneira:
Dentro dos componentes que precisam de localização, existe uma pasta @translations
, que contém en.json, ru, etc.
Como resultado, ao inverter, tudo será coletado em um arquivo, levando em consideração o caminho para a pasta @translations
. O pacote de localização estará em dist / langs / e a configuração será nomeada como config. $ {Some-random} .json.
Em seguida, garantiremos que o pacote de localização desejado seja carregado no aplicativo. Há um momento frágil - apenas o webpack sabe sobre o caminho para as localizações e o nome do arquivo de configuração, vamos levar isso em consideração para que os dados mais recentes entrem no AppTranslateLoader e não seja necessário alterar os nomes em dois lugares.
// some inmports // ... // momentJs import * as moment from 'moment'; import 'moment/locale/en-gb'; import 'moment/locale/ru'; @Injectable() export class AppTranslateLoader { // public additionalStorageKey: string = ''; private translationsDir: string; private translationsConfig: string; private selectedLang: string; private fallbackLang: string; private loadedLang: string; private config: { [key: string]: string; } = null; private loadSubs = new Subscription(); private configSubs = new Subscription(); private loadSubj = new Subject(); private get storageKey(): string { return this.additionalStorageKey ? `APP_LANG_${this.additionalStorageKey}` : 'APP_LANG'; } constructor(private http: HttpClient, private translate: TranslateService) { // webpack // . this.translationsDir = `${process.env.TRANSLATE_OUTPUT}`; this.translationsConfig = `${process.env.TRANSLATE_CONFIG}`; this.fallbackLang = 'en'; const storedLang = this.getUsedLanguage(); if (storedLang) { this.selectedLang = storedLang; } else { this.selectedLang = translate.getBrowserLang() || this.fallbackLang; } } }
process.env.TRANSLATE_OUTPUT
simplesmente não funcionará, precisamos declarar outro plugin no webpack (DefinePlugin ou EnvironmentPlugin):
Agora podemos mudar o caminho para localizações e o nome da configuração em apenas um lugar.
Por padrão, a partir da venda Angular padrão gerada no assembly da Webpack ( ng eject
), não é possível especificar process.env.someValue
partir do código (mesmo se você usar o DefinePlugin), o compilador pode jurar. Para que isso funcione, é necessário atender às condições 2a:
- em main.ts adicione a 1ª linha
/// <reference types="node"/>
- O package.json deve ter
@types/node
- npm install --save-dev @types/node
.
Prosseguimos diretamente para o processo de inicialização.
Se você pretende usar APP_INITIALIZER, retorne Promise, não Observable. Nossa tarefa é escrever uma cadeia de consultas:
- Primeiro você precisa baixar o config.json (apenas se não estiver carregado).
- tente carregar o idioma, que é o idioma do navegador do usuário
- Forneça lógica de fallback com o idioma de download padrão.
// imports @Injectable() AppTranslateLoader { // fields ... // , // , // Subscription unsubscribe // private loadSubs = new Subscription(); private configSubs = new Subscription(); // - // Subject private loadSubj = new Subject(); // constructor ... // Promise! public loadTranslation(lang: string = ''): Promise<any> { if (!lang) { lang = this.selectedLang; } // if (lang === this.loadedLang) { return; } if (!this.config) { this.configSubs.unsubscribe(); this.configSubs = this.http.get<Response>(`${this.translationsDir}${this.translationsConfig}`) .subscribe((config: any) => { this.config = config; this.loadAndUseLang(lang); }); } else { this.loadAndUseLang(lang); } return this.loadSubj.asObservable().toPromise(); } private loadAndUseLang(lang: string) { this.loadSubs.unsubscribe(); this.loadSubs = this.http.get<Response>(`${this.translationsDir}${this.config[lang] || this.config[this.fallbackLang]}`) .subscribe(res => { this.translate.setTranslation(lang, res); this.translate.use(lang).subscribe(() => { this.onLangLoaded(lang); }, // fallback ngx-translate (err) => this.onLoadLangError(lang, err)); }, // fallback http (err) => this.onLoadLangError(lang, err)); } private onLangLoaded(newLang: string) { // if (this.loadedLang && this.loadedLang !== newLang) { this.translate.resetLang(this.loadedLang); } this.loadedLang = newLang; this.selectedLang = newLang; // TODO: // , // en ru, momentJs en. moment().locale(newLang); // . momentJs localStorage.setItem(this.storageKey, newLang); // ls this.loadSubj.complete(); // - . } private onLoadLangError(langKey: string, error: any) { // , if (this.loadedLang) { this.translate.use(this.loadedLang) .subscribe( () => this.onLangLoaded(this.loadedLang), (err) => this.loadSubj.error(err)); // } else if (langKey !== this.fallbackLang) { // fallback this.loadAndUseLang(this.fallbackLang); } else { // this.loadSubj.error(error); } }
Feito.
Agora, voltemos ao problema de mover componentes para outras pastas, encapsulamento e semelhança de polimorfismo.
De fato, já temos algum tipo de encapsulamento. As localizações são colocadas em pastas ao lado dos componentes, todos os caminhos de chave são únicos, mas ainda podemos localizar as chaves do componente some-component1 dentro de some-component2 e será difícil acompanhar tudo, descobriremos mais adiante.
<some-component1 [someLabel]="'components.some-component2.some_key' | tanslate"></some-component1> // components.some-component2 -
Em relação ao movimento de componentes:
Agora, a chave que usaremos na exibição está rigidamente vinculada ao caminho relativo do arquivo de localização e depende da infraestrutura específica do projeto.
Vou dar um caso bastante triste dessa situação:
<div translate="+lazy-module.components.article-component.article_title"></div>
Mas e se eu mudar o nome da pasta do componente para pós-componente?
Será muito difícil inserir essa chave em todos os lugares necessários. Obviamente, ninguém cancelou copiar e colar e encontrar e substituir, mas escrever isso sem as instruções do IDE também é estressante.
Para resolver esses problemas, vamos prestar atenção no que o webpack está fazendo sobre isso? O Webpack possui um carregador , existem muitos carregadores disponíveis que operam nos caminhos de arquivos: por exemplo, caminhos de recursos em css - graças ao webpack, podemos especificar os caminhos relativos da imagem de fundo: url (../ relative.png) e assim por diante. o restante dos caminhos de arquivo no projeto estão em toda parte!
Quem criou seu webpack cria sabe que o carregador recebe um arquivo na entrada que corresponde a um determinado padrão. A tarefa do próprio carregador é, de alguma forma, transformar esse arquivo de entrada e devolvê-lo, para alterações adicionais por outros carregadores.
Portanto, precisamos escrever nosso carregador. A questão é que tipo de arquivo vamos alterar: visualizações ou componentes? Por um lado, as visualizações podem estar diretamente no componente e separadamente. As visualizações podem ser grandes o suficiente e difíceis de analisar, imagine se tivermos uma visualização em que 100 diretivas de conversão (não em loop):
<div id="1">{{'./some_key_1' | translate}}</div> ... <div id="100">{{'../another_key_!' | translate}}</div>
através do carregador, podemos substituir o caminho principal para localizações de componentes perto de cada tubo ou diretiva.
<div id="1">{{'app.some-component.some_key_1' | translate}}</div>
podemos adicionar um campo a um componente que fornece localização:
@Component({ selector: 'app-some', template: '<div>{{(localization + 'key') | tanslate}}</div>' }) export class SomeComponent { localization = './' }
Também é ruim - você precisa compor uma chave de localização em qualquer lugar.
Como as opções mais óbvias parecem ruins, tente usar um decorador e salve alguns metadados no protótipo do componente (como o Angular).

anotações - metadados para decoradores angulares
__app_annotations__ - metadados que armazenaremos para nós mesmos
O caminho para a pasta de localização em relação ao componente pode ser gravado no decorador, o mesmo decorador pode ser expandido com outras opções, exceto o caminho.
//translate.service.ts const app_annotations_key = '__app_annotations__'; export function Localization(path: string) { // tslint:disable-next-line:only-arrow-functions return function (target: Function) { const metaKey = app_annotations_key; Object.defineProperty(target, metaKey, { value: { // path. path, name: 'Translate' } } as PropertyDescriptor); }; } //some.component.ts @Component({...}) @Localization({ path: './', otherOptions: {...} }); export class SomeComponent { }
webpack, loader , - . , ( styleUrls) . loader, npm . .
, -. , -.
<div>{{'just_key' | translate}}</div>
. , , , . — Injector, . , Injector, '' , translate . Injector, ( ), 'get'.

, parent , , Injector'a , , , , , .
, API, forwarRef() ( Angular reactive forms, control ). , . .
// translate.service.ts export const TRANSLATE_TOKEN = new InjectionToken('MyTranslateToken'); // app.component.ts @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [{provide: TRANSLATE_TOKEN, useExisting: forwardRef(() => AppComponent)}] }) @Localization('./') export class AppComponent { title = 'app'; }
, , , forwardRef().
, Injector forwardRef() , . , '' . , , .
// my-translate.directive.ts @Directive({ // tslint:disable-next-line:directive-selector selector: '[myTranslate]' }) export class MyTranslateDirective extends TranslateDirective { @Input() public set myTranslate(e: string) { this.translate = e; } private keyPath: string; constructor(private _translateService: TranslateService, private _element: ElementRef, _chRef: ChangeDetectorRef, // forwardRef() @Inject(TRANSLATE_TOKEN) @Optional() protected cmp: Object) { super(_translateService, _element, _chRef); // const prototype = Object.getPrototypeOf(cmp || {}).constructor; if (prototype[app_annotations_key]) { // this.keyPath = prototype[app_annotations_key].path; } } public updateValue(key: string, node: any, translations: any) { if (this.keyPath) { // , // key = `${this.keyPath.replace(/\//, '.')}.${key}`; } super.updateValue(key, node, translations); } }
.
- :
<div>{{'just_this_component_key' | myTranslate}}</div> // <div myTranslate="just_this_component_key"></div>
translate , . , , - :
//en.bundle.json { "global_key": "Global key" "app-component": { "just_key": "Just key" } } //some-view.html <div translate="global_key"></div>
Research and improve!
full example
:
- FE node.js stacktrace.js.
- Jest Angular .
- Web worker ) , , Angular .