Jelajahi Sumber

feat(admin-ui): Complete UI i18n

Add state for UI language, add script to auto-extract keys into translation files, replace all hard-coded strings with keys.
Michael Bromley 7 tahun lalu
induk
melakukan
8c3b77ce47
34 mengubah file dengan 490 tambahan dan 138 penghapusan
  1. 12 0
      admin-ui/README.md
  2. 2 1
      admin-ui/angular.json
  3. 4 1
      admin-ui/package.json
  4. 19 3
      admin-ui/src/app/app.module.ts
  5. 2 2
      admin-ui/src/app/app.routes.ts
  6. 2 1
      admin-ui/src/app/catalog/catalog.routes.ts
  7. 7 7
      admin-ui/src/app/catalog/components/product-detail/product-detail.component.html
  8. 5 4
      admin-ui/src/app/catalog/components/product-list/product-list.component.html
  9. 1 0
      admin-ui/src/app/core/components/app-shell/app-shell.component.html
  10. 1 1
      admin-ui/src/app/core/components/breadcrumb/breadcrumb.component.html
  11. 6 6
      admin-ui/src/app/core/components/main-nav/main-nav.component.html
  12. 10 0
      admin-ui/src/app/core/components/ui-language-switcher/ui-language-switcher.component.html
  13. 16 0
      admin-ui/src/app/core/components/ui-language-switcher/ui-language-switcher.component.scss
  14. 32 0
      admin-ui/src/app/core/components/ui-language-switcher/ui-language-switcher.component.ts
  15. 1 1
      admin-ui/src/app/core/components/user-menu/user-menu.component.html
  16. 12 1
      admin-ui/src/app/core/core.module.ts
  17. 44 0
      admin-ui/src/app/core/providers/i18n/custom-http-loader.ts
  18. 0 43
      admin-ui/src/app/core/providers/i18n/custom-loader.ts
  19. 3 26
      admin-ui/src/app/core/providers/i18n/i18n.service.ts
  20. 7 0
      admin-ui/src/app/core/providers/i18n/mark-for-extraction.ts
  21. 6 2
      admin-ui/src/app/data/client-state/client-defaults.ts
  22. 13 2
      admin-ui/src/app/data/client-state/client-resolvers.ts
  23. 6 0
      admin-ui/src/app/data/mutations/local-mutations.ts
  24. 22 3
      admin-ui/src/app/data/providers/client-data.service.ts
  25. 8 0
      admin-ui/src/app/data/queries/local-queries.ts
  26. 6 0
      admin-ui/src/app/data/types/client-types.graphql
  27. 33 0
      admin-ui/src/app/data/types/gql-generated-types.ts
  28. 4 6
      admin-ui/src/app/login/components/login/login.component.html
  29. 1 1
      admin-ui/src/app/shared/components/table-row-action/table-row-action.component.html
  30. 1 0
      admin-ui/src/app/shared/components/table-row-action/table-row-action.component.ts
  31. 0 6
      admin-ui/src/i18n-messages/common.messages.ts
  32. 32 0
      admin-ui/src/i18n-messages/en.json
  33. 172 21
      admin-ui/yarn.lock
  34. 0 0
      schema.json

+ 12 - 0
admin-ui/README.md

@@ -15,3 +15,15 @@ All queries should be located in the [`./src/app/data/queries`](src/app/data/que
 
 Run `yarn generate-gql-types` to generate TypeScript interfaces based on these queries. The generated
 types are located at [`./src/app/data/types/gql-generated-types.ts`](src/app/data/types/gql-generated-types.ts).
+
+## Localization
+
+Localization of UI strings is handled by [ngx-translate](http://www.ngx-translate.com/). The translation strings should use the [ICU MessageFormat](http://userguide.icu-project.org/formatparse/messages).
+
+Translation keys are automatically extracted by running:
+```
+yarn extract-translations
+```
+This will add any new translation keys to the default language file located in [`./src/i18n-messages/en.json`](./src/i18n-messages/en.json).
+
+From this master translation file, other language versions can be created by copying and updating the values for the new language.

+ 2 - 1
admin-ui/angular.json

@@ -24,7 +24,8 @@
             "tsConfig": "src/tsconfig.app.json",
             "assets": [
               "src/favicon.ico",
-              "src/assets"
+              "src/assets",
+              "src/i18n-messages"
             ],
             "styles": [
               "./node_modules/@clr/icons/clr-icons.min.css",

+ 4 - 1
admin-ui/package.json

@@ -9,7 +9,8 @@
     "lint": "ng lint vendure-admin --fix",
     "e2e": "ng e2e",
     "apollo": "apollo",
-    "generate-gql-types": "ts-node generate-graphql-types.ts"
+    "generate-gql-types": "ts-node generate-graphql-types.ts",
+    "extract-translations": "ngx-translate-extract --input ./src --output ./src/i18n-messages/en.json --clean --sort --format namespaced-json --format-indentation \"  \" -m _"
   },
   "private": true,
   "dependencies": {
@@ -26,6 +27,7 @@
     "@clr/icons": "^0.11.21",
     "@clr/ui": "^0.11.21",
     "@ngx-translate/core": "^10.0.2",
+    "@ngx-translate/http-loader": "^3.0.1",
     "@webcomponents/custom-elements": "1.0.0",
     "apollo-angular": "^1.1.1",
     "apollo-angular-cache-ngrx": "^1.0.0-beta.0",
@@ -49,6 +51,7 @@
     "@angular/cli": "~6.0.7",
     "@angular/compiler-cli": "^6.0.3",
     "@angular/language-service": "^6.0.3",
+    "@biesbjerg/ngx-translate-extract": "^2.3.4",
     "@types/jasmine": "~2.8.6",
     "@types/jasminewd2": "~2.0.3",
     "@types/node": "~8.9.4",

+ 19 - 3
admin-ui/src/app/app.module.ts

@@ -1,3 +1,4 @@
+import { HttpClient } from '@angular/common/http';
 import { NgModule } from '@angular/core';
 import { BrowserModule } from '@angular/platform-browser';
 import { RouterModule } from '@angular/router';
@@ -6,7 +7,13 @@ import { TranslateMessageFormatDebugCompiler } from 'ngx-translate-messageformat
 import { AppComponent } from './app.component';
 import { routes } from './app.routes';
 import { CoreModule } from './core/core.module';
-import { CustomLoader } from './core/providers/i18n/custom-loader';
+import { CustomHttpTranslationLoader } from './core/providers/i18n/custom-http-loader';
+import { DataService } from './data/providers/data.service';
+import { LanguageCode } from './data/types/gql-generated-types';
+
+export function HttpLoaderFactory(http: HttpClient) {
+    return new CustomHttpTranslationLoader(http, '/i18n-messages/');
+}
 
 @NgModule({
     declarations: [
@@ -16,7 +23,11 @@ import { CustomLoader } from './core/providers/i18n/custom-loader';
         BrowserModule,
         RouterModule.forRoot(routes, { useHash: false }),
         TranslateModule.forRoot({
-            loader: { provide: TranslateLoader, useClass: CustomLoader },
+            loader: {
+                provide: TranslateLoader,
+                useFactory: HttpLoaderFactory,
+                deps: [HttpClient],
+            },
             compiler: { provide: TranslateCompiler, useClass: TranslateMessageFormatDebugCompiler },
         }),
         CoreModule,
@@ -24,4 +35,9 @@ import { CustomLoader } from './core/providers/i18n/custom-loader';
     providers: [],
     bootstrap: [AppComponent],
 })
-export class AppModule {}
+export class AppModule {
+
+    constructor(private dataService: DataService) {
+        this.dataService.client.setUiLanguage(LanguageCode.en);
+    }
+}

+ 2 - 2
admin-ui/src/app/app.routes.ts

@@ -1,7 +1,7 @@
 import { Route } from '@angular/router';
-
 import { AppShellComponent } from './core/components/app-shell/app-shell.component';
 import { AuthGuard } from './core/providers/guard/auth.guard';
+import { _ } from './core/providers/i18n/mark-for-extraction';
 
 export const routes: Route[] = [
     { path: 'login', loadChildren: './login/login.module#LoginModule' },
@@ -10,7 +10,7 @@ export const routes: Route[] = [
         canActivate: [AuthGuard],
         component: AppShellComponent,
         data: {
-            breadcrumb: 'Dashboard',
+            breadcrumb: _('breadcrumb.dashboard'),
         },
         children: [
             {

+ 2 - 1
admin-ui/src/app/catalog/catalog.routes.ts

@@ -1,5 +1,6 @@
 import { Route } from '@angular/router';
 import { map } from 'rxjs/operators';
+import { _ } from '../core/providers/i18n/mark-for-extraction';
 import { DataService } from '../data/providers/data.service';
 import { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
@@ -30,7 +31,7 @@ export function productBreadcrumb(data: any, params: any, dataService: DataServi
         map(productData => {
             return [
                    {
-                       label: 'Products',
+                       label: _('breadcrumb.products'),
                        link: ['../', 'products'],
                    },
                    {

+ 7 - 7
admin-ui/src/app/catalog/components/product-detail/product-detail.component.html

@@ -1,7 +1,7 @@
 <clr-dropdown>
     <button type="button" class="btn btn-outline-primary" clrDropdownTrigger>
         <clr-icon shape="world"></clr-icon>
-        Language: {{ languageCode$ | async | uppercase }}
+        {{ 'common.language' }}: {{ languageCode$ | async | uppercase }}
         <clr-icon shape="caret down"></clr-icon>
     </button>
     <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
@@ -15,23 +15,23 @@
 <form class="form" [formGroup]="productForm" (ngSubmit)="save()">
     <button class="btn btn-primary"
             type="submit"
-            [disabled]="productForm.invalid || productForm.pristine">Update</button>
+            [disabled]="productForm.invalid || productForm.pristine">{{ 'common.update' | translate }}</button>
 
     <section class="form-block">
-        <label>Product</label>
-        <vdr-form-field label="Product name" for="name">
+        <label>{{ 'catalog.product' | translate }}</label>
+        <vdr-form-field [label]="'catalog.product-name' | translate" for="name">
             <input id="name" type="text" formControlName="name">
         </vdr-form-field>
-        <vdr-form-field label="Slug" for="slug">
+        <vdr-form-field [label]="'catalog.slug' | translate" for="slug">
             <input id="slug" type="text" formControlName="slug">
         </vdr-form-field>
-        <vdr-form-field label="Description" for="description">
+        <vdr-form-field [label]="'catalog.description' | translate" for="description">
             <textarea id="description" formControlName="description"></textarea>
         </vdr-form-field>
     </section>
 
     <section class="form-block">
-        <label>Product Variants</label>
+        <label>{{ 'catalog.product-variants' | translate }}</label>
     </section>
 
 </form>

+ 5 - 4
admin-ui/src/app/catalog/components/product-list/product-list.component.html

@@ -4,10 +4,10 @@
                 [currentPage]="currentPage$ | async"
                 (pageChange)="setPageNumber($event)"
                 (itemsPerPageChange)="setItemsPerPage($event)">
-    <vdr-dt-column>ID</vdr-dt-column>
-    <vdr-dt-column>Name</vdr-dt-column>
-    <vdr-dt-column>Slug</vdr-dt-column>
-    <vdr-dt-column>Description</vdr-dt-column>
+    <vdr-dt-column>{{ 'catalog.ID' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'catalog.name' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'catalog.slug' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'catalog.description' | translate }}</vdr-dt-column>
     <vdr-dt-column></vdr-dt-column>
     <ng-template let-product="item">
         <td class="left">{{ product.id }}</td>
@@ -16,6 +16,7 @@
         <td class="left">{{ product.description }}</td>
         <td class="right">
             <vdr-table-row-action iconShape="edit"
+                                  [label]="'common.edit' | translate"
                                   [linkTo]="['./', product.id]">
             </vdr-table-row-action>
         </td>

+ 1 - 0
admin-ui/src/app/core/components/app-shell/app-shell.component.html

@@ -12,6 +12,7 @@
 
         </div>
         <div class="header-actions">
+            <vdr-ui-language-switcher></vdr-ui-language-switcher>
             <vdr-user-menu [userName]="userName$ | async"
                            (logOut)="logOut()"></vdr-user-menu>
         </div>

+ 1 - 1
admin-ui/src/app/core/components/breadcrumb/breadcrumb.component.html

@@ -1,4 +1,4 @@
-<nav aria-label="You are here:" role="navigation">
+<nav role="navigation">
     <ul class="breadcrumbs">
         <li *ngFor="let breadcrumb of breadcrumbs$ | async; let isLast = last">
             <a [routerLink]="breadcrumb.link" *ngIf="!isLast">{{ breadcrumb.label }}</a>

+ 6 - 6
admin-ui/src/app/core/components/main-nav/main-nav.component.html

@@ -2,33 +2,33 @@
     <section class="sidenav-content">
         <section class="nav-group">
             <input id="tabexample2" type="checkbox">
-            <label for="tabexample2">Catalog</label>
+            <label for="tabexample2">{{ 'nav.catalog' | translate }}</label>
             <ul class="nav-list">
                 <li>
                     <a class="nav-link"
                        [routerLink]="['/catalog', 'products']"
                        routerLinkActive="active">
-                        <clr-icon shape="library" size="20"></clr-icon>Products
+                        <clr-icon shape="library" size="20"></clr-icon>{{ 'nav.products' | translate }}
                     </a>
                 </li>
                 <li><a class="nav-link">
-                    <clr-icon shape="tag" size="20"></clr-icon>Facets
+                    <clr-icon shape="tag" size="20"></clr-icon>{{ 'nav.facets' | translate }}
                 </a>
                 </li>
                 <li>
                     <a class="nav-link">
-                        <clr-icon shape="folder-open" size="20"></clr-icon>Categories
+                        <clr-icon shape="folder-open" size="20"></clr-icon>{{ 'nav.categories' | translate }}
                     </a>
                 </li>
             </ul>
         </section>
         <section class="nav-group">
             <input id="tabexample2" type="checkbox">
-            <label for="tabexample2">Sales</label>
+            <label for="tabexample2">{{ 'nav.sales' | translate }}</label>
             <ul class="nav-list">
                 <li>
                     <a class="nav-link">
-                        <clr-icon shape="shopping-cart" size="20"></clr-icon>Orders
+                        <clr-icon shape="shopping-cart" size="20"></clr-icon>{{ 'nav.orders' | translate }}
                     </a>
                 </li>
             </ul>

+ 10 - 0
admin-ui/src/app/core/components/ui-language-switcher/ui-language-switcher.component.html

@@ -0,0 +1,10 @@
+<clr-dropdown>
+    <span class="user-name">{{ uiLanguage$ | async }}</span>
+    <span class="trigger" clrDropdownTrigger>
+        <clr-icon shape="caret down"></clr-icon>
+    </span>
+    <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
+        <button type="button" clrDropdownItem (click)="setLanguage('en')">EN</button>
+        <button type="button" clrDropdownItem (click)="setLanguage('de')">DE</button>
+    </clr-dropdown-menu>
+</clr-dropdown>

+ 16 - 0
admin-ui/src/app/core/components/ui-language-switcher/ui-language-switcher.component.scss

@@ -0,0 +1,16 @@
+:host {
+    display: flex;
+    align-items: center;
+    margin: 0 0.5rem;
+    height: 2.5rem;
+}
+
+.user-name {
+    color: lightgrey;
+    margin-right: 12px;
+}
+
+.trigger clr-icon {
+    color: white;
+}
+

+ 32 - 0
admin-ui/src/app/core/components/ui-language-switcher/ui-language-switcher.component.ts

@@ -0,0 +1,32 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { Observable } from 'rxjs';
+import { map, tap } from 'rxjs/operators';
+import { DataService } from '../../../data/providers/data.service';
+import { LanguageCode } from '../../../data/types/gql-generated-types';
+import { I18nService } from '../../providers/i18n/i18n.service';
+
+@Component({
+    selector: 'vdr-ui-language-switcher',
+    templateUrl: './ui-language-switcher.component.html',
+    styleUrls: ['./ui-language-switcher.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class UiLanguageSwitcherComponent implements OnInit {
+
+    uiLanguage$: Observable<LanguageCode>;
+
+    constructor(private dataService: DataService,
+                private i18nService: I18nService) { }
+
+    ngOnInit() {
+        this.uiLanguage$ = this.dataService.client.uiState().stream$.pipe(
+            map(data => data.uiState.language),
+            tap(languageCode => this.i18nService.setLanguage(languageCode)),
+        );
+    }
+
+    setLanguage(languageCode: LanguageCode) {
+        this.dataService.client.setUiLanguage(languageCode).subscribe();
+    }
+
+}

+ 1 - 1
admin-ui/src/app/core/components/user-menu/user-menu.component.html

@@ -5,6 +5,6 @@
         <clr-icon shape="caret down"></clr-icon>
     </span>
     <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
-        <button type="button" clrDropdownItem (click)="logOut.emit()">Log out</button>
+        <button type="button" clrDropdownItem (click)="logOut.emit()">{{ 'common.log-out' | translate }}</button>
     </clr-dropdown-menu>
 </clr-dropdown>

+ 12 - 1
admin-ui/src/app/core/core.module.ts

@@ -6,9 +6,11 @@ import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.componen
 import { MainNavComponent } from './components/main-nav/main-nav.component';
 import { NotificationComponent } from './components/notification/notification.component';
 import { OverlayHostComponent } from './components/overlay-host/overlay-host.component';
+import { UiLanguageSwitcherComponent } from './components/ui-language-switcher/ui-language-switcher.component';
 import { UserMenuComponent } from './components/user-menu/user-menu.component';
 import { AuthService } from './providers/auth/auth.service';
 import { AuthGuard } from './providers/guard/auth.guard';
+import { I18nService } from './providers/i18n/i18n.service';
 import { LocalStorageService } from './providers/local-storage/local-storage.service';
 import { NotificationService } from './providers/notification/notification.service';
 import { OverlayHostService } from './providers/overlay-host/overlay-host.service';
@@ -26,10 +28,19 @@ import { OverlayHostService } from './providers/overlay-host/overlay-host.servic
         LocalStorageService,
         AuthGuard,
         AuthService,
+        I18nService,
         OverlayHostService,
         NotificationService,
     ],
-    declarations: [AppShellComponent, UserMenuComponent, MainNavComponent, BreadcrumbComponent, OverlayHostComponent, NotificationComponent],
+    declarations: [
+        AppShellComponent,
+        UserMenuComponent,
+        MainNavComponent,
+        BreadcrumbComponent,
+        OverlayHostComponent,
+        NotificationComponent,
+        UiLanguageSwitcherComponent,
+    ],
     entryComponents: [NotificationComponent],
 })
 export class CoreModule {}

+ 44 - 0
admin-ui/src/app/core/providers/i18n/custom-http-loader.ts

@@ -0,0 +1,44 @@
+import { HttpClient } from '@angular/common/http';
+import { TranslateLoader } from '@ngx-translate/core';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+export type Dictionary = {
+    [key: string]: string | Dictionary;
+};
+
+/**
+ * A loader for ngx-translate which extends the HttpLoader functionality by stripping out any
+ * values which are empty strings. This means that during development, translation keys which have
+ * been extracted but not yet defined will fall back to the raw key text rather than displaying nothing.
+ *
+ * Orignally from https://github.com/ngx-translate/core/issues/662#issuecomment-377010232
+ */
+export class CustomHttpTranslationLoader implements TranslateLoader {
+    constructor(private http: HttpClient, private prefix: string = '/assets/i18n/', private suffix: string = '.json') {
+    }
+
+    public getTranslation(lang: string): Observable<any> {
+        return this.http.get(`${this.prefix}${lang}${this.suffix}`).pipe(
+            map((res: any) => this.process(res)));
+    }
+
+    private process(object: Dictionary): Dictionary {
+        const newObject: Dictionary = {};
+
+        for (const key in object) {
+            if (object.hasOwnProperty(key)) {
+                const value = object[key];
+                if (typeof value !== 'string') {
+                    newObject[key] = this.process(value);
+                } else if ((typeof value === 'string') && (value === '')) {
+                    // do not copy empty strings
+                } else {
+                    newObject[key] = object[key];
+                }
+            }
+        }
+
+        return newObject;
+    }
+}

+ 0 - 43
admin-ui/src/app/core/providers/i18n/custom-loader.ts

@@ -1,43 +0,0 @@
-import { TranslateLoader } from '@ngx-translate/core';
-import { Observable, of as observableOf } from 'rxjs';
-
-declare function require(path: string): any;
-
-const translations = [
-    'common',
-    // 'error',
-    // 'notification',
-].reduce((hash, name) => {
-    hash[name] = require(`../../../../i18n-messages/${name}.messages.ts`).default;
-    return hash;
-}, {} as { [name: string]: any; });
-
-/**
- * A custom language loader which splits apart a translations object in the format:
- * {
- *   SECTION: {
- *     TOKEN: {
- *       lang1: "...",
- *       lang2: "....
- *     }
- *   }
- * }
- */
-export class CustomLoader implements TranslateLoader {
-
-    getTranslation(lang: string): Observable<any> {
-        const output: any = {};
-        for (const section in translations) {
-            if (translations.hasOwnProperty(section)) {
-                output[section] = {};
-
-                for (const token in translations[section]) {
-                    if (translations[section].hasOwnProperty(token)) {
-                        output[section][token] = translations[section][token][lang];
-                    }
-                }
-            }
-        }
-        return observableOf(output);
-    }
-}

+ 3 - 26
admin-ui/src/app/core/providers/i18n/i18n.service.ts

@@ -1,18 +1,17 @@
 import { Injectable } from '@angular/core';
 import { TranslateService } from '@ngx-translate/core';
-
-export type UILanguage = 'en' | 'de';
+import { LanguageCode } from '../../../data/types/gql-generated-types';
 
 @Injectable()
 export class I18nService {
     constructor(private ngxTranslate: TranslateService) {
-        // ngxTranslate.setDefaultLang(config.FALLBACK_LANGUAGE);
+        ngxTranslate.setDefaultLang(LanguageCode.en);
     }
 
     /**
      * Set the UI language
      */
-    setLanguage(language: UILanguage): void {
+    setLanguage(language: LanguageCode): void {
         this.ngxTranslate.use(language);
     }
 
@@ -22,26 +21,4 @@ export class I18nService {
     translate(key: string | string[], params?: any): string {
         return this.ngxTranslate.instant(key, params);
     }
-
-    /**
-     * Attempt to infer the user language from the browser's navigator object. If the result is not
-     * amongst the valid UI languages, default to the fallback language instead.
-     */
-    /*inferUserLanguage(): UILanguage {
-        const browserLanguage = navigator.language.split('-')[0];
-        if (this.config.UI_LANGUAGES.indexOf(browserLanguage) >= 0) {
-            return browserLanguage as any;
-        }
-
-        if ((navigator as any).languages) {
-            const languages: string[] = (navigator as any).languages;
-            for (const lang of languages.map(l => l.split('-')[0])) {
-                if (this.config.UI_LANGUAGES.indexOf(lang) >= 0) {
-                    return lang as any;
-                }
-            }
-        }
-
-        return this.config.FALLBACK_LANGUAGE as any;
-    }*/
 }

+ 7 - 0
admin-ui/src/app/core/providers/i18n/mark-for-extraction.ts

@@ -0,0 +1,7 @@
+/**
+ * The purpose of this function is to mark strings for extraction by ngx-translate-extract.
+ * See https://github.com/biesbjerg/ngx-translate-extract/tree/7d5d38e6a17c2232407bf6b0bc65808d5f81208d#mark-strings-for-extraction-using-a-marker-function
+ */
+export function _(key: string | string[]): string | string[] {
+	return key;
+}

+ 6 - 2
admin-ui/src/app/data/client-state/client-defaults.ts

@@ -1,6 +1,6 @@
-import { GetNetworkStatus, GetUserStatus } from '../types/gql-generated-types';
+import { GetNetworkStatus, GetUiState, GetUserStatus, LanguageCode } from '../types/gql-generated-types';
 
-export const clientDefaults: GetNetworkStatus & GetUserStatus = {
+export const clientDefaults: GetNetworkStatus & GetUserStatus & GetUiState = {
     networkStatus: {
         inFlightRequests: 0,
         __typename: 'NetworkStatus',
@@ -11,4 +11,8 @@ export const clientDefaults: GetNetworkStatus & GetUserStatus = {
         loginTime: '',
         __typename: 'UserStatus',
     },
+    uiState: {
+        language: LanguageCode.en,
+        __typename: 'UiState',
+    },
 };

+ 13 - 2
admin-ui/src/app/data/client-state/client-resolvers.ts

@@ -3,10 +3,11 @@ import { GraphQLFieldResolver } from 'graphql';
 import { GET_NEWTORK_STATUS } from '../queries/local-queries';
 import {
     GetNetworkStatus,
+    GetUiState,
     GetUserStatus,
-    GetUserStatus_userStatus,
+    GetUserStatus_userStatus, LanguageCode,
     LogInVariables,
-    RequestStarted,
+    SetUiLanguageVariables,
 } from '../types/gql-generated-types';
 
 export type ResolverContext = {
@@ -54,6 +55,16 @@ export const clientResolvers: ResolverDefinition = {
             cache.writeData({ data });
             return data.userStatus;
         },
+        setUiLanguage: (_, args: SetUiLanguageVariables, { cache }): LanguageCode => {
+            const data: GetUiState = {
+                uiState: {
+                    __typename: 'UiState',
+                    language: args.languageCode,
+                },
+            };
+            cache.writeData({ data });
+            return args.languageCode;
+        },
     },
 };
 

+ 6 - 0
admin-ui/src/app/data/mutations/local-mutations.ts

@@ -31,3 +31,9 @@ export const LOG_OUT = gql`
         }
     }
 `;
+
+export const SET_UI_LANGUAGE = gql`
+    mutation SetUiLanguage($languageCode: LanguageCode!) {
+        setUiLanguage(languageCode: $languageCode) @client
+    }
+`;

+ 22 - 3
admin-ui/src/app/data/providers/client-data.service.ts

@@ -1,7 +1,19 @@
 import { Observable } from 'rxjs';
-import { LOG_IN, LOG_OUT, REQUEST_COMPLETED, REQUEST_STARTED } from '../mutations/local-mutations';
-import { GET_NEWTORK_STATUS, GET_USER_STATUS } from '../queries/local-queries';
-import { GetNetworkStatus, GetUserStatus, LogIn, LogInVariables, LogOut, RequestCompleted, RequestStarted } from '../types/gql-generated-types';
+import { LOG_IN, LOG_OUT, REQUEST_COMPLETED, REQUEST_STARTED, SET_UI_LANGUAGE } from '../mutations/local-mutations';
+import { GET_NEWTORK_STATUS, GET_UI_STATE, GET_USER_STATUS } from '../queries/local-queries';
+import {
+    GetNetworkStatus,
+    GetUiState,
+    GetUserStatus,
+    LanguageCode,
+    LogIn,
+    LogInVariables,
+    LogOut,
+    RequestCompleted,
+    RequestStarted,
+    SetUiLanguage,
+    SetUiLanguageVariables,
+} from '../types/gql-generated-types';
 import { QueryResult } from '../types/query-result';
 import { BaseDataService } from './base-data.service';
 
@@ -36,4 +48,11 @@ export class ClientDataService {
         return this.baseDataService.query<GetUserStatus>(GET_USER_STATUS);
     }
 
+    uiState(): QueryResult<GetUiState> {
+        return this.baseDataService.query<GetUiState>(GET_UI_STATE);
+    }
+
+    setUiLanguage(languageCode: LanguageCode): Observable<SetUiLanguage> {
+        return this.baseDataService.mutate<SetUiLanguage, SetUiLanguageVariables>(SET_UI_LANGUAGE, { languageCode });
+    }
 }

+ 8 - 0
admin-ui/src/app/data/queries/local-queries.ts

@@ -17,3 +17,11 @@ export const GET_USER_STATUS = gql`
         }
     }
 `;
+
+export const GET_UI_STATE = gql`
+    query GetUiState {
+        uiState @client {
+            language
+        }
+    }
+`;

+ 6 - 0
admin-ui/src/app/data/types/client-types.graphql

@@ -1,6 +1,7 @@
 type Query {
     networkStatus: NetworkStatus!
     userStatus: UserStatus!
+    uiState: UiState!
 }
 
 type Mutation {
@@ -8,6 +9,7 @@ type Mutation {
     requestCompleted: Int!
     logIn(username: String!, loginTime: String!): UserStatus
     logOut: UserStatus
+    setUiLanguage(languageCode: LanguageCode): LanguageCode
 }
 
 type NetworkStatus {
@@ -19,3 +21,7 @@ type UserStatus {
     isLoggedIn: Boolean!
     loginTime: String!
 }
+
+type UiState {
+    language: LanguageCode!
+}

+ 33 - 0
admin-ui/src/app/data/types/gql-generated-types.ts

@@ -67,6 +67,22 @@ export interface LogOut {
 }
 
 
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: SetUiLanguage
+// ====================================================
+
+export interface SetUiLanguage {
+  setUiLanguage: LanguageCode | null;
+}
+
+export interface SetUiLanguageVariables {
+  languageCode: LanguageCode;
+}
+
+
 /* tslint:disable */
 // This file was automatically generated and should not be edited.
 
@@ -166,6 +182,23 @@ export interface GetUserStatus {
 }
 
 
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: GetUiState
+// ====================================================
+
+export interface GetUiState_uiState {
+  __typename: "UiState";
+  language: LanguageCode;
+}
+
+export interface GetUiState {
+  uiState: GetUiState_uiState;
+}
+
+
 /* tslint:disable */
 // This file was automatically generated and should not be edited.
 

+ 4 - 6
admin-ui/src/app/login/components/login/login.component.html

@@ -2,8 +2,6 @@
     <form class="login">
         <label class="title">
             <img src="/assets/cube-logo-300px.png">
-            <h3 class="welcome">Welcome to</h3>
-            vendure
         </label>
         <div class="login-group">
             <input class="username"
@@ -11,23 +9,23 @@
                    name="username"
                    id="login_username"
                    [(ngModel)]="username"
-                   placeholder="Username">
+                   [placeholder]="'common.username' | translate">
             <input class="password"
                    name="password"
                    type="password"
                    id="login_password"
                    [(ngModel)]="password"
-                   placeholder="Password">
+                   [placeholder]="'common.password' | translate">
             <div class="checkbox">
                 <input type="checkbox" id="rememberme">
                 <label for="rememberme">
-                    Remember me
+                    {{ 'common.remember-me' | translate }}
                 </label>
             </div>
             <div class="error active" *ngIf="lastError">
                 {{ lastError }}
             </div>
-            <button type="submit" class="btn btn-primary" (click)="logIn()">NEXT</button>
+            <button type="submit" class="btn btn-primary" (click)="logIn()">{{ 'common.login' | translate }}</button>
         </div>
     </form>
 </div>

+ 1 - 1
admin-ui/src/app/shared/components/table-row-action/table-row-action.component.html

@@ -1,3 +1,3 @@
 <a class="btn btn-link btn-sm" [routerLink]="linkTo">
-    <clr-icon [attr.shape]="iconShape"></clr-icon> Edit
+    <clr-icon [attr.shape]="iconShape"></clr-icon> {{ label }}
 </a>

+ 1 - 0
admin-ui/src/app/shared/components/table-row-action/table-row-action.component.ts

@@ -10,5 +10,6 @@ import { Component, Input, OnInit } from '@angular/core';
 })
 export class TableRowActionComponent {
     @Input() linkTo: any[];
+    @Input() label: string;
     @Input() iconShape: string;
 }

+ 0 - 6
admin-ui/src/i18n-messages/common.messages.ts

@@ -1,6 +0,0 @@
-export default {
-  THINGS: {
-    en: `There {count, plural, =0{is} one{is} other{are} } {count, plural, =0{nothing} one{one thing} other{# things} }`,
-    de: `Es gibt {count, plural, =0{keine Dinge} one{ein Ding} other{# Dinge} }`,
-  },
-};

+ 32 - 0
admin-ui/src/i18n-messages/en.json

@@ -0,0 +1,32 @@
+{
+  "breadcrumb": {
+    "dashboard": "",
+    "products": ""
+  },
+  "catalog": {
+    "ID": "",
+    "description": "",
+    "name": "",
+    "product": "",
+    "product-name": "",
+    "product-variants": "",
+    "slug": ""
+  },
+  "common": {
+    "edit": "",
+    "log-out": "",
+    "login": "",
+    "password": "",
+    "remember-me": "",
+    "update": "",
+    "username": ""
+  },
+  "nav": {
+    "catalog": "",
+    "categories": "",
+    "facets": "",
+    "orders": "",
+    "products": "",
+    "sales": ""
+  }
+}

+ 172 - 21
admin-ui/yarn.lock

@@ -193,6 +193,21 @@
     lodash "^4.2.0"
     to-fast-properties "^2.0.0"
 
+"@biesbjerg/ngx-translate-extract@^2.3.4":
+  version "2.3.4"
+  resolved "https://registry.yarnpkg.com/@biesbjerg/ngx-translate-extract/-/ngx-translate-extract-2.3.4.tgz#f0b661e8227e63374c72e41775e0a73b6b4fec64"
+  dependencies:
+    chalk "2.0.1"
+    cheerio "1.0.0-rc.2"
+    flat "2.0.1"
+    fs "0.0.1-security"
+    gettext-parser "1.2.2"
+    glob "7.1.2"
+    mkdirp "0.5.1"
+    path "0.12.7"
+    typescript "2.4.1"
+    yargs "8.0.2"
+
 "@clr/angular@^0.11.21":
   version "0.11.21"
   resolved "https://registry.yarnpkg.com/@clr/angular/-/angular-0.11.21.tgz#af004553c3c48a0f1b2c87018adf2244dadb5ef8"
@@ -237,6 +252,12 @@
   dependencies:
     tslib "^1.9.0"
 
+"@ngx-translate/http-loader@^3.0.1":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-3.0.1.tgz#20b0f98bc6c25321129d3e3302ab3cc489c0a42a"
+  dependencies:
+    tslib "^1.9.0"
+
 "@nodelib/fs.stat@^1.0.1":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.0.tgz#50c1e2260ac0ed9439a181de3725a0168d59c48a"
@@ -1559,6 +1580,14 @@ center-align@^0.1.1:
     align-text "^0.1.3"
     lazy-cache "^1.0.3"
 
+chalk@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d"
+  dependencies:
+    ansi-styles "^3.1.0"
+    escape-string-regexp "^1.0.5"
+    supports-color "^4.0.0"
+
 chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -1608,6 +1637,17 @@ change-case@^3.0.1:
     upper-case "^1.1.1"
     upper-case-first "^1.1.0"
 
+cheerio@1.0.0-rc.2:
+  version "1.0.0-rc.2"
+  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
+  dependencies:
+    css-select "~1.2.0"
+    dom-serializer "~0.1.0"
+    entities "~1.1.1"
+    htmlparser2 "^3.9.1"
+    lodash "^4.15.0"
+    parse5 "^3.0.1"
+
 chokidar@^1.4.1, chokidar@^1.4.2:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
@@ -2080,7 +2120,7 @@ css-parse@1.7.x:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-1.7.0.tgz#321f6cf73782a6ff751111390fc05e2c657d8c9b"
 
-css-select@^1.1.0:
+css-select@^1.1.0, css-select@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
   dependencies:
@@ -2366,7 +2406,7 @@ dom-serialize@^2.2.0:
     extend "^3.0.0"
     void-elements "^2.0.0"
 
-dom-serializer@0:
+dom-serializer@0, dom-serializer@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
   dependencies:
@@ -2377,7 +2417,7 @@ domain-browser@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
 
-domelementtype@1:
+domelementtype@1, domelementtype@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
 
@@ -2391,6 +2431,12 @@ domhandler@2.1:
   dependencies:
     domelementtype "1"
 
+domhandler@^2.3.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+  dependencies:
+    domelementtype "1"
+
 domutils@1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
@@ -2404,6 +2450,13 @@ domutils@1.5.1:
     dom-serializer "0"
     domelementtype "1"
 
+domutils@^1.5.1:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
 dot-case@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-2.1.1.tgz#34dcf37f50a8e93c2b3bca8bb7fb9155c7da3bee"
@@ -2465,7 +2518,7 @@ encodeurl@~1.0.1, encodeurl@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
 
-encoding@^0.1.11:
+encoding@0.1.12, encoding@^0.1.11:
   version "0.1.12"
   resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
   dependencies:
@@ -2528,7 +2581,7 @@ ent@~2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
 
-entities@~1.1.1:
+entities@^1.1.1, entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
 
@@ -2975,12 +3028,18 @@ find-up@^1.0.0:
     path-exists "^2.0.0"
     pinkie-promise "^2.0.0"
 
-find-up@^2.1.0:
+find-up@^2.0.0, find-up@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
   dependencies:
     locate-path "^2.0.0"
 
+flat@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/flat/-/flat-2.0.1.tgz#70e29188a74be0c3c89409eed1fa9577907ae32f"
+  dependencies:
+    is-buffer "~1.1.2"
+
 flush-write-stream@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd"
@@ -3092,6 +3151,10 @@ fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
 
+fs@0.0.1-security:
+  version "0.0.1-security"
+  resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4"
+
 fsevents@^1.0.0, fsevents@^1.1.2:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
@@ -3163,6 +3226,12 @@ getpass@^0.1.1:
   dependencies:
     assert-plus "^1.0.0"
 
+gettext-parser@1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/gettext-parser/-/gettext-parser-1.2.2.tgz#1ef0da75c1e759ae3089c73efa4d19e40298748e"
+  dependencies:
+    encoding "0.1.12"
+
 git-parse@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/git-parse/-/git-parse-1.0.3.tgz#82f165201892688ec9286184b3eee5c4cf0655ac"
@@ -3214,6 +3283,17 @@ glob@7.0.x:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 glob@^5.0.15:
   version "5.0.15"
   resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
@@ -3234,17 +3314,6 @@ glob@^6.0.4:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
-  version "7.1.2"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.4"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
 globals@^9.18.0:
   version "9.18.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
@@ -3619,6 +3688,17 @@ html-webpack-plugin@^3.0.6:
     toposort "^1.0.0"
     util.promisify "1.0.0"
 
+htmlparser2@^3.9.1:
+  version "3.9.2"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
+  dependencies:
+    domelementtype "^1.3.0"
+    domhandler "^2.3.0"
+    domutils "^1.5.1"
+    entities "^1.1.1"
+    inherits "^2.0.1"
+    readable-stream "^2.0.2"
+
 htmlparser2@~3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
@@ -3861,7 +3941,7 @@ is-binary-path@^1.0.0:
   dependencies:
     binary-extensions "^1.0.0"
 
-is-buffer@^1.1.5:
+is-buffer@^1.1.5, is-buffer@~1.1.2:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
 
@@ -4582,6 +4662,15 @@ load-json-file@^1.0.0:
     pinkie-promise "^2.0.0"
     strip-bom "^2.0.0"
 
+load-json-file@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^2.2.0"
+    pify "^2.0.0"
+    strip-bom "^3.0.0"
+
 loader-runner@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
@@ -4647,7 +4736,7 @@ lodash@^3.8.0:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
-lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.5.0, lodash@~4.17.10:
+lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.5.0, lodash@~4.17.10:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 
@@ -4991,7 +5080,7 @@ mixin-object@^2.0.1:
     for-in "^0.1.3"
     is-extendable "^0.1.1"
 
-mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
+mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
@@ -5563,6 +5652,12 @@ parse-json@^2.2.0:
   dependencies:
     error-ex "^1.2.0"
 
+parse5@^3.0.1:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+  dependencies:
+    "@types/node" "*"
+
 parse5@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
@@ -5659,12 +5754,25 @@ path-type@^1.0.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
+path-type@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+  dependencies:
+    pify "^2.0.0"
+
 path-type@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
   dependencies:
     pify "^3.0.0"
 
+path@0.12.7:
+  version "0.12.7"
+  resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
+  dependencies:
+    process "^0.11.1"
+    util "^0.10.3"
+
 pbkdf2@^3.0.3:
   version "3.0.16"
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c"
@@ -5809,7 +5917,7 @@ process-nextick-args@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
 
-process@^0.11.10:
+process@^0.11.1, process@^0.11.10:
   version "0.11.10"
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
 
@@ -6037,6 +6145,13 @@ read-pkg-up@^1.0.1:
     find-up "^1.0.0"
     read-pkg "^1.0.0"
 
+read-pkg-up@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+  dependencies:
+    find-up "^2.0.0"
+    read-pkg "^2.0.0"
+
 read-pkg@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
@@ -6045,6 +6160,14 @@ read-pkg@^1.0.0:
     normalize-package-data "^2.3.2"
     path-type "^1.0.0"
 
+read-pkg@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+  dependencies:
+    load-json-file "^2.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^2.0.0"
+
 "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.6:
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
@@ -7316,6 +7439,10 @@ typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 
+typescript@2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc"
+
 "typescript@>=2.6.2 <2.8", typescript@~2.7.2:
   version "2.7.2"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836"
@@ -7886,6 +8013,12 @@ yargs-parser@^5.0.0:
   dependencies:
     camelcase "^3.0.0"
 
+yargs-parser@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+  dependencies:
+    camelcase "^4.1.0"
+
 yargs-parser@^9.0.2:
   version "9.0.2"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
@@ -7909,6 +8042,24 @@ yargs@11.0.0:
     y18n "^3.2.1"
     yargs-parser "^9.0.2"
 
+yargs@8.0.2:
+  version "8.0.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+  dependencies:
+    camelcase "^4.1.0"
+    cliui "^3.2.0"
+    decamelize "^1.1.1"
+    get-caller-file "^1.0.1"
+    os-locale "^2.0.0"
+    read-pkg-up "^2.0.0"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^2.0.0"
+    which-module "^2.0.0"
+    y18n "^3.2.1"
+    yargs-parser "^7.0.0"
+
 yargs@^7.0.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"

File diff ditekan karena terlalu besar
+ 0 - 0
schema.json


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini