From 4a6e1ba5328bb449ca1476103225db2ea77fdd30 Mon Sep 17 00:00:00 2001 From: Date: Mon, 12 Aug 2019 12:41:43 -0700 Subject: [PATCH 1/2] moved article to draft folder --- article/{ => draft}/part-1.md | 0 article/{ => draft}/part-2.md | 0 article/{ => draft}/part-3.md | 0 article/{ => draft}/part-4.md | 0 article/{ => draft}/part-5.md | 0 article/{ => draft}/part-6.md | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename article/{ => draft}/part-1.md (100%) rename article/{ => draft}/part-2.md (100%) rename article/{ => draft}/part-3.md (100%) rename article/{ => draft}/part-4.md (100%) rename article/{ => draft}/part-5.md (100%) rename article/{ => draft}/part-6.md (100%) diff --git a/article/part-1.md b/article/draft/part-1.md similarity index 100% rename from article/part-1.md rename to article/draft/part-1.md diff --git a/article/part-2.md b/article/draft/part-2.md similarity index 100% rename from article/part-2.md rename to article/draft/part-2.md diff --git a/article/part-3.md b/article/draft/part-3.md similarity index 100% rename from article/part-3.md rename to article/draft/part-3.md diff --git a/article/part-4.md b/article/draft/part-4.md similarity index 100% rename from article/part-4.md rename to article/draft/part-4.md diff --git a/article/part-5.md b/article/draft/part-5.md similarity index 100% rename from article/part-5.md rename to article/draft/part-5.md diff --git a/article/part-6.md b/article/draft/part-6.md similarity index 100% rename from article/part-6.md rename to article/draft/part-6.md From 59f985809702ddd65e19defb0f459b023a385794 Mon Sep 17 00:00:00 2001 From: Date: Mon, 12 Aug 2019 12:45:33 -0700 Subject: [PATCH 2/2] deleted drafts --- article/draft/part-1.md | 248 ---------------------------------------- article/draft/part-2.md | 169 --------------------------- article/draft/part-3.md | 78 ------------- article/draft/part-4.md | 93 --------------- article/draft/part-5.md | 151 ------------------------ article/draft/part-6.md | 152 ------------------------ 6 files changed, 891 deletions(-) delete mode 100644 article/draft/part-1.md delete mode 100644 article/draft/part-2.md delete mode 100644 article/draft/part-3.md delete mode 100644 article/draft/part-4.md delete mode 100644 article/draft/part-5.md delete mode 100644 article/draft/part-6.md diff --git a/article/draft/part-1.md b/article/draft/part-1.md deleted file mode 100644 index 7250f4e..0000000 --- a/article/draft/part-1.md +++ /dev/null @@ -1,248 +0,0 @@ -# Lost in Translation... Strings - -> Image - -# Part 1 of 6 - i18n for Angular Applications - -## 🤔 The Background on Being Lost - -So, I'm a married. While I love my wife and love being married, marriage can be difficult. Communication can be difficult. Not to mention the added layer of English being my second language and my wife's first language. - -Thinking about curtailing my communication with my wife is an ongoing process that has allowed me to make a relevant associative connection to an issue I was having with translation strings and code maintainability. In order to find a more encompassing solution to this difficult issue, I took deep dive into i18n for Angular applications. - -What does i18n stand for and why is there an "18" in the middle? Even as a front-end engineer I had no idea until I looked it up. It's the number of letters between "i" and "n" in the word "internationalization." So, i18n is internationalization. Pretty neat. One of the [definitions](https://www.w3.org/International/questions/qa-i18n) of i18n is: - -> The design and development of a product, application or document content that **enables** easy localization for target audiences that vary in culture, region, or language. - -As I researched more and more about i18n, or internationalization, I realized that this concept hit home, pun intended, in more of a direct way that I thought. Many of the minor disagreements that me and my wife get into have so much to do with not only words getting lost in translation, but how her and I translate each other's words back to ourselves internally. Improving communication in marriage is and will always be a work in progress with the main goal being quite similar to that of i18n. The goal being to enable easier understanding on the other end and for my wife and me to know that the intentions of what we mean to convey (the "source code") is separate from the sometimes fumbled words that we choose to use (the "translation strings"). - -By following the link above, we can see that there are multiple areas of development that i18n touches on. However, the area we'll concentrate on in this article is: - -> Separating localizable elements from source code or content, such that localized alternatives can be loaded or selected based on the user's international preferences as needed. - -In essence, whatever should be displayed in different languages needs to be separated out from the meat of the code to enable its maintainability. - -In the article, we will apply i18n capabilities to a simple Angular application, so that we can see and discover how separating translation strings from the source code can improve maintainability. - -We will explore how to implement our translation strings in a maintainable manner, enable the application to load only necessary resources, and allow browser memorization of the selected language. Then we will enable Server-Side Rendering (SSR) and solve issues encountered during enabling SSR in the Angular application. - -The article is split up in the following parts: -Part 1. Setting the Scene -Part 2. -Part 3. -Part 4. -Part 5. -Part 6. - -In the first part of this article, we will follow simple instructions for setting up an Angular application and adding i18n capabilities to it. Beginner-level developers may want to take a deep dive into the sections that follow. More advanced developers may glance at the code in the following sections and proceed to the "Adding SSR to the App" part to find out what obstacles adding SSR will create and how to solve them. - -## 📝 Setting the Scene - -For the purposes of this article, we'll work with a bare-bones Angular application generated with [AngularCLI](https://cli.angular.io/). To follow along with the article, we will generate an app using the command (assuming the Angular CLI installed globally): - -``` -ng new ssr-with-i18n -``` - -For the sake of the example let's add a couple of components: -``` -ng g c comp-a -ng g c comp-b -``` - -Now, we will replace the contents of app.component.html with these two components: - -```html -

Welcome to {{ title }}!

- - -``` - -*** The code up to this point is available [here](https://github.com/DmitryEfimenko/ssr-with-i18n/tree/step-1). - -## 🗺️ Let's Add i18n - -As with most things in coding, there are many ways to skin a cat. In this article, we'll use a popular library: [ngx-translate](https://github.com/ngx-translate/core). - -Originally, I wanted to use the framework-independent library [i18next](https://www.i18next.com/) with an Angular wrapper: [angular-i18next](https://github.com/Romanchuk/angular-i18next). However, there is currently a [big limitation](https://github.com/Romanchuk/angular-i18next/pull/11#issuecomment-364725022) with angular-i18next: it's not capable of switching language on the fly, which is a show-stopper for me. - -Using ngx-translate will allow us to store our strings in separate JSON files (a file per language) where each string will be represented by a key-value pair. The key is a string identifier and the value is the translation of the string. - -1. Install Dependencies - -In addition to the core library, we'll install the http-loader library which will enable loading translations on-demand. - -``` -npm install @ngx-translate/core @ngx-translate/http-loader --save -``` - -2. Add the Code - -The directions for the ngx-translate package suggest adding relevant code directly to the AppModule. However, I think we can do better. Let's create a separate module that will encapsulate i18n related logic. - -``` -ng g m i18n --module app -``` - -This will add a new file: `/i18n/i18n.module.ts` and reference it in the `app.module.ts`. - -Modify the `i18n.module.ts` file according to the [documentation](https://github.com/ngx-translate/core#configuration). The full file code is below. - -```ts -import { NgModule } from '@angular/core'; -import { HttpClient, HttpClientModule } from '@angular/common/http'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; - -@NgModule({ - imports: [ - HttpClientModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: translateLoaderFactory, - deps: [HttpClient] - } - }), - ], - exports: [TranslateModule] -}) -export class I18nModule { - constructor(translate: TranslateService) { - translate.addLangs(['en', 'ru']); - const browserLang = translate.getBrowserLang(); - translate.use(browserLang.match(/en|ru/) ? browserLang : 'en'); - } -} - -export function translateLoaderFactory(httpClient: HttpClient) { - return new TranslateHttpLoader(httpClient); -} -``` - -Nothing fancy is going on. We just added the `TranslateModule` and configured it to use the `HttpClient` to load translations. We exported `TranslateModule` as well to make the pipe `transform` available in the `AppModule`. In the constructor, we specified available languages and used a built-in function to get and use the browser's default language. - -By default, the `TranslateHttpLoader` will load translations from the `/assets/i18n/` folder, so let's add a couple of files there. - -**/assets/i18n/en.json** -```json -{ - "compA": "Component A works", - "compB": "Component B works" -} -``` -/assets/i18n/ru.json -```json -{ - "compA": "Компонент А работает", - "compB": "Компонент Б работает" -} -``` - -With this configuration, we should be able to update our component templates to use the translation strings instead of hard-coded text. - -```html -// comp-a.component.html -

{{'compA' | translate}}

- -// comp-b.component.html -

{{'compB' | translate}}

-``` - -Run the application and notice that it's using the translations from the `en.json` file. Let's add a component that will let us switch between the two languages. - -``` -ng g c select-language --inlineStyle --inlineTemplate -``` - -Update the contents of the select-language.component.ts file. - -```ts -import { Component } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; - -@Component({ - selector: 'app-select-language', - template: ` - - `, -}) -export class SelectLanguageComponent { - constructor(public translate: TranslateService) { } -} -``` - -The ngx-translate library allows us to switch languages via a simple `translate.use()` API call. It also allows us to determine the currently selected language by querying the `translate.currentLang` property. - -Insert the new component in the `app.component.html` file after the `h1` tag. - -```html -

Welcome to {{ title }}!

- - - -``` - -Run the application and see that the language can now be switched on the fly. Selecting a different language will request the appropriate `.json` file. - -> Animation showing things working - -Now, if we select the language `ru` and refresh the browser, we'll see that the page loaded with the language `en` selected. The browser does not have a mechanism for remembering the selected language. Let's fix that. - -## 🙄 Memorizing the Selected Language - -The Angular community has made many [plugins](https://github.com/ngx-translate/core#plugins) enhancing the functionality of the ngx-translate package. One of them is exactly what we need - [ngx-translate-cache](https://github.com/jgpacheco/ngx-translate-cache). By following instructions, we'll (1) install the package - -``` -npm install ngx-translate-cache --save -``` - -and (2) use it inside of the I18nModule. - -```ts -import { TranslateCacheModule, TranslateCacheSettings, TranslateCacheService } from 'ngx-translate-cache'; - -@NgModule({ - imports: [ - TranslateModule.forRoot(...), // unchanged - TranslateCacheModule.forRoot({ - cacheService: { - provide: TranslateCacheService, - useFactory: translateCacheFactory, - deps: [TranslateService, TranslateCacheSettings] - }, - cacheMechanism: 'Cookie' - }) - ] -}) -export class I18nModule { - constructor( - translate: TranslateService, - translateCacheService: TranslateCacheService - ) { - translateCacheService.init(); - translate.addLangs(['en', 'ru']); - const browserLang = translateCacheService.getCachedLanguage() || translate.getBrowserLang(); - translate.use(browserLang.match(/en|ru/) ? browserLang : 'en'); - } -} - -export function translateCacheFactory( - translateService: TranslateService, - translateCacheSettings: TranslateCacheSettings -) { - return new TranslateCacheService(translateService, translateCacheSettings); -} -``` - -Now, if we select the language `ru` and refresh the browser we'll see that it remembered our choice. Notice, that we selected `'Cookies'` as a place to store the selected language. The default selection for this option is `'LocalStorage'`. However, LocalStorage is not accessible on the server. A big part of this article has to do with enabling SSR, so we're being a little bit proactive here and storing the language selection in the place where a server can read it. - -Up until now there really was nothing special about this article. We simply followed instructions posted in relevant packages and encapsulated the i18n related logic in a separate module. Adding SSR has some tricky parts, so let's take a closer look. - -*** The code up to this point is available [here](https://github.com/DmitryEfimenko/ssr-with-i18n/tree/step-2). diff --git a/article/draft/part-2.md b/article/draft/part-2.md deleted file mode 100644 index a9620be..0000000 --- a/article/draft/part-2.md +++ /dev/null @@ -1,169 +0,0 @@ -## 💪️ Part 2 of 6: Adding SSR to the App - -Angular CLI is amazing! In particular, its schematics feature allows us to add new capabilities to the app using a simple command. In this case, we'll run the following command to add SSR capabilities: - -``` -ng add @nguniversal/express-engine --clientProject ssr-with-i18n -``` - -Running this command updated and added a few files. - -> Image showing new files - -If we look at the `package.json` file, we'll see that now we have a few new scripts that we can execute. The two most important are: (1) `build:ssr` and (2) `serve:ssr`. Let's run these commands and see what happens. - -Both commands run successfully. However, when we load the website in the browser, we get an error. - -``` -TypeError: Cannot read property 'match' of undefined - at new I18nModule (C:\Source\Random\ssr-with-i18n\dist\server\main.js:113153:35) -``` - -A little bit of investigation reveals that the failing code is: - -```ts -browserLang.match(/en|ru/) -``` - -The `browserLang` variable is undefined, which means that the following line of code didn't work: - -```ts -const browserLang = translateCacheService.getCachedLanguage() || translate.getBrowserLang(); -``` - -This happens because we're trying to access browser-specific APIs during the server-side rendering. Even the name of the function - `getBrowserLang()` suggests that this function won't work on the server. We'll come back to this issue, but for the time being, let's patch it by hard-coding the value of the `browserLang` variable: - -```ts -const browserLang = 'en'; -``` - -Build and serve the application again. This time there is no error. In fact, if we look at the network tab of the Developer Tools we'll see that SSR worked! However, the translations didn't come through. - -> Screenshot showing SSR partially working - -Let's see why this is happening. Notice the factory function used in the `TranslateModule` to load translations: `translateLoaderFactory`. This function makes use of the `HttpClient` and knows how to load the JSON files containing translations from the browser. However, the factory function is not smart enough to know how to load these files while in the server environment. - -This brings us to the two issues we need to solve: - -ISSUE 1. Being able to determine the correct language to load in both the client and the server environments (instead of hard-coding the value to `en`). -ISSUE 2. Based on the environment, use the appropriate mechanism to load the JSON file containing translations. - -Now that the issues are identified, let's examine different ways to solve these issues. - -## 🤔 Evaluating Existing Options - -There are a few ways that we can make everything work. There is a closed issue in the ngx-translate repository related to enabling SSR - [issue #754](https://github.com/ngx-translate/core/issues/754). A few solutions to these issues can be found there. - -### Existing Solution 1. Fix via HttpInterceptor - -One of the latest comments to the issue #754 suggests using a solution found in the article "[Angular Universal: How to add multi language support?](https://itnext.io/angular-universal-how-to-add-multi-language-support-68d83f6dfc4d)" to address the ISSUE 2. Unfortunately, ISSUE 1 is not addressed in the article. The author suggests a fix using the `HttpInterceptor`, which patches the requests to the JSON files while on the server. - -Even though the solution works, it feels a bit awkward to me to create an interceptor that will patch the path of the request. In addition, why should we be making an extra request (even though it's local) when we have access to the files through the file system? Let's see what other options are available. - -### Existing Solution 2. Fix via Importing JSON Files Directly - -A few recent comments on the same [issue #754](https://github.com/ngx-translate/core/issues/754) suggest importing the contents of JSON files straight into the file which defines our module. Then we can check which environment we're running in and either use the default `TranslateHttpLoader` or a custom one, which uses the imported JSON. This approach suggests a way to handle ISSUE 2 by checking the environment where the code is running: `if (isPlatformBrowser(platform))`. We'll use a similar platform check later in the article. - -```ts -import { PLATFORM_ID } from "@angular/core"; -import { isPlatformBrowser } from '@angular/common'; -import * as translationEn from './assets/i18n/en.json'; -import * as translationEs from './assets/i18n/es.json'; - -const TRANSLATIONS = { - en: translationEn, - es: translationEs, -}; - -export class JSONModuleLoader implements TranslateLoader { - getTranslation(lang: string): Observable { - return of(TRANSLATIONS[lang]); - } -} - -export function translateLoaderFactory(http: HttpClient, platform: any) { - if (isPlatformBrowser(platform)) { - return new TranslateHttpLoader(http); - } else { - return new JSONModuleLoader(); - } -} - -// module imports: -TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: translateLoaderFactory, - deps: [HttpClient, PLATFORM_ID] - } -}) -``` - -**Please don't do this!** By importing JSON files like shown above, they will end up in the browser bundle. The whole purpose of using HttpLoader is that it will load the required language file **on demand** making the browser bundle smaller. - -With this method, the translations for all the languages will be bundled together with the run-time JavaScript compromising performance. - -Although both existing solutions provide a fix for ISSUE 2, they have their shortcomings. One results in unnecesary requests being made and another one compromizes performance. Neither of them provide a solution for ISSUE 1. - -## 🔋 A Better Way - Prerequisites -In the upcoming sections I'll provide two separate solutions to the identified ISSUES. Both of the solutions will require the following prerequisites. - -Prerequisite 1. We need to install and use a dependency called [cookie-parser](https://www.npmjs.com/package/cookie-parser). -Prerequisite 2. Understand the REQUEST injection token - -### Prerequisite 1. Why Do We Need cookie-parser? -In the upcoming solutions we'll need a way to access the cookie on the server. This cookie is named "lang" and is set in the browser when a user chooses a language. By default we can access the information we need from the `req.headers.cookie` object in any of the Express request handlers. The value would look something like this: - -``` -lang=en; other-cookie=other-value -``` - -This property has all the information we need, but we need to parse the `lang` out. Although it's simple enough, there is no need to reinvent the wheel since `cookie-parser` is an Express middleware that does exactly what we need. - -Install the required dependencies. - -``` -npm install cookie-parser -npm install @types/cookie-parser -D -``` - -Update the `server.ts` file to use the installed `cookie-parser`. - -```ts -import * as cookieParser from 'cookie-parser'; -app.use(cookieParser()); -``` - -Under the hood, the `cookie-parser` will parse the Cookies and store them as a dictionary object under `req.cookies`. - -```json -{ - "lang": "en", - "other-cookie": "other-value" -} -``` - -### Prerequisite 2. The REQUEST Injection Token -Now that we have a convenient way of accessing Cookies from the request object, we need to have access to the `req` object in the context of the Angular application. This can easily be done using the `REQUEST` injection token. - -```ts -import { REQUEST } from '@nguniversal/express-engine/tokens'; -import { Request } from 'express'; - -export class AnyModule { - constructor(@Inject(REQUEST) private req: Request) { - console.log(req.cookies.lang); // 'en' | 'ru' - } -} -``` - -Here's the obvious fact: The `REQUEST` injection token is available under `@nguniversal/express-engine/tokens`. Here is a not so obvious fact: the type for the `req` object is the `Request` provided by type definitions of the `express` library. - -This is important and might trip us over. If this import is forgotten, the typescript will assume a different `Request` type from the Fetch API that's available under `lib.dom.d.ts`. As a result, TypeScript will not have knowledge of `req.cookies` object and will underline it with red. - - -## Now We Are Ready for the Solutions -Please make a mental snapshot of the STEP 3 Checkpoint below. We will use this code as a starting point for the next two parts of this series where we'll explore how to fix the two ISSUES outlined above. - -### STEP 3 Checkpoint -*** The code up to this point is available [here](https://github.com/DmitryEfimenko/ssr-with-i18n/tree/step-3). diff --git a/article/draft/part-3.md b/article/draft/part-3.md deleted file mode 100644 index bf709fc..0000000 --- a/article/draft/part-3.md +++ /dev/null @@ -1,78 +0,0 @@ -# Lost in Translation... Strings -# Part 3 of 6: i18n for Server-Side Rendered Angular Applications - -## 👌 Solution 1. Fix via Providing a Separate I18nModule for the Server - -Currently, our application looks like this: - -> Image - -The diagram above shows the path of code execution when the code runs in the browser (green) and when it runs in the server (blue). Notice, that in the case of a browser, the file that bootstraps the whole application (`main.ts`) imports the `AppModule` directly. In the case of the server, the main file imports a separate module, the `AppServerModule`, which in turn imports the `AppModule`. Also, notice that the `I18nModule` is a dependency of `AppModule`, which means that the code of `I18nModule` will be executed in both the client and in the server. - -In the solution below we'll make the browser side look more like the server side. We'll introduce a new module - the `AppBrowserModule`. That will be the module to be bootstrapped. We'll also rename the I18nModule to `I18nBrowserModule` and move it into the imports of the `AppBrowserModule`. Finally, we'll introduce a new module, the `I18nServerModule`, that will use file system access to load JSON files. This module will be imported inside of the `AppServerModule`. See the resulting structure below: - -> Image - -Below is the code of the new I18nServerModule. - -```ts -import { Inject, NgModule } from '@angular/core'; -import { REQUEST } from '@nguniversal/express-engine/tokens'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Request } from 'express'; -import { readFileSync } from 'fs'; -import { join } from 'path'; -import { Observable, of } from 'rxjs'; - -@NgModule({ - imports: [ - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: translateFSLoaderFactory - } - }) - ] -}) -export class I18nServerModule { - constructor(translate: TranslateService, @Inject(REQUEST) req: Request) { - translate.addLangs(['en', 'ru']); - const language: 'en' | 'ru' = req.cookies.lang || 'en'; - translate.use(language.match(/en|ru/) ? language : 'en'); - } -} -``` - -```ts -export class TranslateFSLoader implements TranslateLoader { - constructor(private prefix = 'i18n', private suffix = '.json') { } - - public getTranslation(lang: string): Observable { - const path = join(__dirname, '../browser/assets/', this.prefix, `${lang}${this.suffix}`); - const data = JSON.parse(readFileSync(path, 'utf8')); - return of(data); - } -} - -export function translateFSLoaderFactory() { - return new TranslateFSLoader(); -} -``` - -There are two main things happening in the code above. - -First, we make use of the REQUEST injection token provided by Angular to get a hold of the full request object. Then we access the cookies object to find out what language the user selected in the browser. We call the `use` method of the `TranslateService` class so that our website gets rendered in that language. - -Second, the action above will trigger our custom loading mechanism defined in the `TranslateFsLoader` class. In the class we simply use standard node API to read files from the file system (fs). - -## Solution 1 Summary - -This solution completely separates the compilation path for the server from the compilation path for the browser. ISSUE 1 is solved due to the `translate.getBrowserLang()` existing only in the `I18nBrowserModule`, which will never run in the server environment. - -ISSUE 2 is similarly solved by each I18n Module -- the Server and the Browser modules -- using their own Translation Loader mechanism - the `TranslateFsLoader` and `TranslateHttpLoader` respectively. - -I like this option because it comes with a clear separation between the code that runs on the server and the code that runs in the browser. Introducing the `AppBrowserModule` establishes the architecture for handling cases when the server- and client-side logic significantly differs. Perhaps this approach is best suited for larger applications. - -However, there is one more approach to tackle this task. Keep reading! - -*** The code up to this point is available [here](https://github.com/DmitryEfimenko/ssr-with-i18n/tree/step-4). diff --git a/article/draft/part-4.md b/article/draft/part-4.md deleted file mode 100644 index 80f74b8..0000000 --- a/article/draft/part-4.md +++ /dev/null @@ -1,93 +0,0 @@ -# Lost in Translation... Strings -# Part 4 of 6: i18n for Server-Side Rendered Angular applications - -## 👌 Solution 2. Provide Everything in a Single Module - -Now that we looked at Solution 1, let's examine another way. In contrast to the Solution 1, this solution does not require the creation of new modules. Instead, all code will be placed inside of the `I18nModule`. This can be achieved using the `isPlatformBrowser` function provided by the Angular framework. - -Let's come back to the [STEP 3 Checkpoint](https://github.com/DmitryEfimenko/ssr-with-i18n/tree/step-3). - -``` -git checkout step-3 -``` - -We'll reuse the `TranslateFSLoader` class we created in the previous step. However, we'll make the `I18nModule` aware of the platform it's running in and use the appropriate Loader depending on the environment. - -Add the `PLATFORM_ID` to the deps of the `translateLoaderFactory`. This will allow us to select the loader in the factory depending on the current platform: - -```ts -export function translateLoaderFactory(httpClient: HttpClient, platform: any) { - return isPlatformBrowser(platform) - ? new TranslateHttpLoader(httpClient) - : new TranslateFSLoader(); -} -``` - -Now, the factory function will use the appropriate Loader depending on the platform. Similar adjustments need to be done to the `constructor` of the `I18nModule`. - -```ts -@NgModule({...}) -export class I18nModule { - constructor( - translate: TranslateService, - translateCacheService: TranslateCacheService, - @Optional() @Inject(REQUEST) private req: Request, - @Inject(PLATFORM_ID) private platform: any - ) { - if (isPlatformBrowser(this.platform)) { - translateCacheService.init(); - } - - translate.addLangs(['en', 'ru']); - - const browserLang = isPlatformBrowser(this.platform) - ? translateCacheService.getCachedLanguage() || translate.getBrowserLang() || 'en' - : this.getLangFromServerSideCookie() || 'en'; - - translate.use(browserLang.match(/en|ru/) ? browserLang : 'en'); - } - - getLangFromServerSideCookie() { - if (this.req) { - return this.req.cookies.lang; - } - } -} -``` - -If we try to build the application now we'll get an error. - -``` -Module not found: Error: Can't resolve 'fs' in 'C:\ssr-with-i18n\src\app\i18n' -Module not found: Error: Can't resolve 'path' in 'C:\ssr-with-i18n\src\app\i18n' -``` - -That's because the `fs` and the `path` dependencies, which are strictly node dependencies, are now referenced in the file that's compiled for the browser environment. - -We, as developers, know that these node dependencies won't be used because they are behind appropriate `if` statements, but the compiler does not know that. - -There is an easy fix for this issue as well. We can let our compiler know not to include these dependencies in the browser environment using a new [browser](https://github.com/defunctzombie/package-browser-field-spec) field of the `package.json` file. - -Add the following to the `package.json` file. - -```json -"browser": { - "fs": false, - "path": false -} -``` - -Now, everything will compile and run exactly the same as with the previous solution. - -## Solution 2 Summary - -Both ISSUE 1 and ISSUE 2 are solved by separating browser-specific code from the server-specific code via an `if` statement that evaluates the current platform: -``` -isPlatformBrowser(this.platform) -``` - -Now that there is only a single path of compilation for both platforms, `fs` and `path` dependencies that are strictly node dependencies cause a compilation-time error when the build process compiles a browser bundle. This is solved by specifying these dependencies in the `browser` field of the `package.json` file and setting their values to `false`. - -I like this option because it's simpler from the perspective of the consumer application. There's no need to create additional modules. - -*** The code up to this point is available [here](https://github.com/DmitryEfimenko/ssr-with-i18n/tree/step-5). diff --git a/article/draft/part-5.md b/article/draft/part-5.md deleted file mode 100644 index c83c8fc..0000000 --- a/article/draft/part-5.md +++ /dev/null @@ -1,151 +0,0 @@ -# Lost in Translation... Strings -# Part 5 of 6: i18n for Server-Side Rendered Angular Applications - -## ⚡️ Improve Performance with TransferState - -If we run our app in its current state and take a look at the network tab of the Chrome Developer Tools, we'll see that after initial load the app will make a request to load the JSON file for the currently selected language. - -This does not make much sense since we've already loaded the appropriate language in the server. - -Making an extra request to load language translations that are already loaded might seem like it's not a big issue worth solving. There probably are areas of an application that will result in a bigger bang for the buck in terms of performance tuning. See [this article](https://christianlydemann.com/the-complete-guide-to-angular-load-time-optimization/) for more on this topic. However, for bigger applications, translation files might also get bigger. Therefore, the time to download and process them will also increase, at which point this would be an issue to solve. - -Thankfully, Angular Universal provides a tool to solve this issue with relatively little effort: `[TransferState](https://angular.io/api/platform-browser/TransferState)` - -### Overview of the Workflow - -To make use of the TransferState feature, we need to: -1. Add `ServerTransferStateModule` in the server and `BrowserTransferStateModule` on the client -2. On the server: set the data that we want to transfer under a specific key using API: `transferState.set(key, value)` -3. On the client: retrieve the data using API: `transferState.get(key, defaultValue)` - -### Our implementation -First, let's add the TransferStateModules to the imports: -```ts -import { BrowserTransferStateModule, TransferState } from '@angular/platform-browser'; - -@NgModule({ - imports: [ - BrowserTransferStateModule, // ADDED - // ... - ] -}) -export class I18nModule { - // ... -} -``` -```ts -import { ServerTransferStateModule } from '@angular/platform-server'; - -@NgModule({ - imports: [ - ServerTransferStateModule, // ADDED - // ... - ], - bootstrap: [AppComponent], -}) -export class AppServerModule { } -``` - -Now let's make appropriate changes to the `I18nModule`. The snippet below shows the new code. - -```ts -// ADDED needed imports from @angular -import { makeStateKey, TransferState } from '@angular/platform-browser'; - -@NgModule({ - imports: [ - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: translateLoaderFactory, - deps: [HttpClient, TransferState, PLATFORM_ID] // ADDED: dependency for the factory func - } - }) - ] -}) -export class I18nModule { - // ... -} -``` - -Second, the `translateLoaderFactory` will not look like this: - -```ts -export function translateLoaderFactory(httpClient: HttpClient, transferState: TransferState, platform: any) { - return isPlatformBrowser(platform) - ? new TranslateHttpLoader(httpClient) - : new TranslateFSLoader(transferState); -} -``` - -TranslateFSLoader will now make use of TransferState: - -```ts -import { makeStateKey, TransferState } from '@angular/platform-browser'; - -export class TranslateFSLoader implements TranslateLoader { - constructor( - // ADDED: inject the transferState service - private transferState: TransferState, - private prefix = 'i18n', - private suffix = '.json' - ) { } - - public getTranslation(lang: string): Observable { - const path = join(__dirname, '../browser/assets/', this.prefix, `${lang}${this.suffix}`); - const data = JSON.parse(readFileSync(path, 'utf8')); - // ADDED: store the translations in the transfer state: - const key = makeStateKey('transfer-translate-' + lang); - this.transferState.set(key, data); - return of(data); - } -} -``` - -How does it exactly transfer the state? During the server-side rendering, the framework will include the data in the HTML `