Explorar o código

feat(admin-ui): Allow configuration of available locales (#2550)

HoseinGhanbari %!s(int64=2) %!d(string=hai) anos
pai
achega
dfddf0fa01

+ 253 - 1
packages/admin-ui-plugin/src/constants.ts

@@ -4,7 +4,7 @@ import path from 'path';
 export const DEFAULT_APP_PATH = path.join(__dirname, '../admin-ui');
 export const loggerCtx = 'AdminUiPlugin';
 export const defaultLanguage = LanguageCode.en;
-export const defaultLocale = undefined;
+export const defaultLocale = 'US';
 
 export const defaultAvailableLanguages = [
     LanguageCode.he,
@@ -26,3 +26,255 @@ export const defaultAvailableLanguages = [
     LanguageCode.ne,
     LanguageCode.hr,
 ];
+
+export const defaultAvailableLocales = [
+    'AF',
+    'AL',
+    'DZ',
+    'AS',
+    'AD',
+    'AO',
+    'AI',
+    'AQ',
+    'AG',
+    'AR',
+    'AM',
+    'AW',
+    'AU',
+    'AT',
+    'AZ',
+    'BS',
+    'BH',
+    'BD',
+    'BB',
+    'BY',
+    'BE',
+    'BZ',
+    'BJ',
+    'BM',
+    'BT',
+    'BO',
+    'BQ',
+    'BA',
+    'BW',
+    'BV',
+    'BR',
+    'IO',
+    'BN',
+    'BG',
+    'BF',
+    'BI',
+    'CV',
+    'KH',
+    'CM',
+    'CA',
+    'KY',
+    'CF',
+    'TD',
+    'CL',
+    'CN',
+    'CX',
+    'CC',
+    'CO',
+    'KM',
+    'CD',
+    'CG',
+    'CK',
+    'CR',
+    'HR',
+    'CU',
+    'CW',
+    'CY',
+    'CZ',
+    'CI',
+    'DK',
+    'DJ',
+    'DM',
+    'DO',
+    'EC',
+    'EG',
+    'SV',
+    'GQ',
+    'ER',
+    'EE',
+    'SZ',
+    'ET',
+    'FK',
+    'FO',
+    'FJ',
+    'FI',
+    'FR',
+    'GF',
+    'PF',
+    'TF',
+    'GA',
+    'GM',
+    'GE',
+    'DE',
+    'GH',
+    'GI',
+    'GR',
+    'GL',
+    'GD',
+    'GP',
+    'GU',
+    'GT',
+    'GG',
+    'GN',
+    'GW',
+    'GY',
+    'HT',
+    'HM',
+    'VA',
+    'HN',
+    'HK',
+    'HU',
+    'IS',
+    'IN',
+    'ID',
+    'IR',
+    'IQ',
+    'IE',
+    'IM',
+    'IL',
+    'IT',
+    'JM',
+    'JP',
+    'JE',
+    'JO',
+    'KZ',
+    'KE',
+    'KI',
+    'KP',
+    'KR',
+    'KW',
+    'KG',
+    'LA',
+    'LV',
+    'LB',
+    'LS',
+    'LR',
+    'LY',
+    'LI',
+    'LT',
+    'LU',
+    'MO',
+    'MG',
+    'MW',
+    'MY',
+    'MV',
+    'ML',
+    'MT',
+    'MH',
+    'MQ',
+    'MR',
+    'MU',
+    'YT',
+    'MX',
+    'FM',
+    'MD',
+    'MC',
+    'MN',
+    'ME',
+    'MS',
+    'MA',
+    'MZ',
+    'MM',
+    'NA',
+    'NR',
+    'NP',
+    'NL',
+    'NC',
+    'NZ',
+    'NI',
+    'NE',
+    'NG',
+    'NU',
+    'NF',
+    'MK',
+    'MP',
+    'NO',
+    'OM',
+    'PK',
+    'PW',
+    'PS',
+    'PA',
+    'PG',
+    'PY',
+    'PE',
+    'PH',
+    'PN',
+    'PL',
+    'PT',
+    'PR',
+    'QA',
+    'RO',
+    'RU',
+    'RW',
+    'RE',
+    'BL',
+    'SH',
+    'KN',
+    'LC',
+    'MF',
+    'PM',
+    'VC',
+    'WS',
+    'SM',
+    'ST',
+    'SA',
+    'SN',
+    'RS',
+    'SC',
+    'SL',
+    'SG',
+    'SX',
+    'SK',
+    'SI',
+    'SB',
+    'SO',
+    'ZA',
+    'GS',
+    'SS',
+    'ES',
+    'LK',
+    'SD',
+    'SR',
+    'SJ',
+    'SE',
+    'CH',
+    'SY',
+    'TW',
+    'TJ',
+    'TZ',
+    'TH',
+    'TL',
+    'TG',
+    'TK',
+    'TO',
+    'TT',
+    'TN',
+    'TR',
+    'TM',
+    'TC',
+    'TV',
+    'UG',
+    'UA',
+    'AE',
+    'GB',
+    'UM',
+    'US',
+    'UY',
+    'UZ',
+    'VU',
+    'VE',
+    'VN',
+    'VG',
+    'VI',
+    'WF',
+    'EH',
+    'YE',
+    'ZM',
+    'ZW',
+    'AX',
+];

+ 18 - 4
packages/admin-ui-plugin/src/plugin.ts

@@ -1,5 +1,8 @@
 import { MiddlewareConsumer, NestModule } from '@nestjs/common';
-import { DEFAULT_AUTH_TOKEN_HEADER_KEY, DEFAULT_CHANNEL_TOKEN_KEY } from '@vendure/common/lib/shared-constants';
+import {
+    DEFAULT_AUTH_TOKEN_HEADER_KEY,
+    DEFAULT_CHANNEL_TOKEN_KEY,
+} from '@vendure/common/lib/shared-constants';
 import {
     AdminUiAppConfig,
     AdminUiAppDevModeConfig,
@@ -27,6 +30,7 @@ import {
     defaultLocale,
     DEFAULT_APP_PATH,
     loggerCtx,
+    defaultAvailableLocales,
 } from './constants';
 import { MetricsService } from './service/metrics.service';
 
@@ -257,8 +261,17 @@ export class AdminUiPlugin implements NestModule {
         const propOrDefault = <Prop extends keyof AdminUiConfig>(
             prop: Prop,
             defaultVal: AdminUiConfig[Prop],
+            isArray: boolean = false,
         ): AdminUiConfig[Prop] => {
-            return partialConfig ? (partialConfig as AdminUiConfig)[prop] || defaultVal : defaultVal;
+            if (isArray) {
+                const isValidArray = !!partialConfig
+                    ? !!((partialConfig as AdminUiConfig)[prop] as any[])?.length
+                    : false;
+
+                return !!partialConfig && isValidArray ? (partialConfig as AdminUiConfig)[prop] : defaultVal;
+            } else {
+                return partialConfig ? (partialConfig as AdminUiConfig)[prop] || defaultVal : defaultVal;
+            }
         };
         return {
             adminApiPath: propOrDefault('adminApiPath', apiOptions.adminApiPath),
@@ -274,11 +287,12 @@ export class AdminUiPlugin implements NestModule {
             ),
             channelTokenKey: propOrDefault(
                 'channelTokenKey',
-                apiOptions.channelTokenKey || DEFAULT_CHANNEL_TOKEN_KEY
+                apiOptions.channelTokenKey || DEFAULT_CHANNEL_TOKEN_KEY,
             ),
             defaultLanguage: propOrDefault('defaultLanguage', defaultLanguage),
             defaultLocale: propOrDefault('defaultLocale', defaultLocale),
-            availableLanguages: propOrDefault('availableLanguages', defaultAvailableLanguages),
+            availableLanguages: propOrDefault('availableLanguages', defaultAvailableLanguages, true),
+            availableLocales: propOrDefault('availableLocales', defaultAvailableLocales, true),
             loginUrl: options.adminUiConfig?.loginUrl,
             brand: options.adminUiConfig?.brand,
             hideVendureBranding: propOrDefault(

+ 10 - 6
packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.ts

@@ -30,6 +30,7 @@ export class AppShellComponent implements OnInit {
     uiLanguageAndLocale$: LocalizationLanguageCodeType;
     direction$: LocalizationDirectionType;
     availableLanguages: LanguageCode[] = [];
+    availableLocales: string[] = [];
     hideVendureBranding = getAppConfig().hideVendureBranding;
     pageTitle$: Observable<string>;
     mainNavExpanded$: Observable<boolean>;
@@ -57,6 +58,8 @@ export class AppShellComponent implements OnInit {
 
         this.availableLanguages = this.i18nService.availableLanguages;
 
+        this.availableLocales = this.i18nService.availableLocales;
+
         this.pageTitle$ = this.breadcrumbService.breadcrumbs$.pipe(
             map(breadcrumbs => breadcrumbs[breadcrumbs.length - 1].label),
         );
@@ -70,17 +73,18 @@ export class AppShellComponent implements OnInit {
         this.uiLanguageAndLocale$
             .pipe(
                 take(1),
-                switchMap(([currentLanguage, currentLocale]) =>
-                    this.modalService.fromComponent(UiLanguageSwitcherDialogComponent, {
+                switchMap(([currentLanguage, currentLocale]) => {
+                    return this.modalService.fromComponent(UiLanguageSwitcherDialogComponent, {
                         closable: true,
                         size: 'lg',
                         locals: {
+                            availableLocales: this.availableLocales,
                             availableLanguages: this.availableLanguages,
-                            currentLanguage,
-                            currentLocale,
+                            currentLanguage: currentLanguage,
+                            currentLocale: currentLocale,
                         },
-                    }),
-                ),
+                    });
+                }),
                 switchMap(result =>
                     result ? this.dataService.client.setUiLanguage(result[0], result[1]) : EMPTY,
                 ),

+ 56 - 53
packages/admin-ui/src/lib/core/src/components/ui-language-switcher-dialog/ui-language-switcher-dialog.component.html

@@ -1,64 +1,67 @@
 <ng-template vdrDialogTitle>{{ 'common.select-display-language' | translate }}</ng-template>
-<div class="clr-row">
-    <div class="flex pl-2 mb-2">
-        <vdr-form-field [label]="'common.language' | translate" class="mr-2">
-            <select name="options" [(ngModel)]="currentLanguage" (ngModelChange)="updatePreviewLocale()">
-                <option *ngFor="let code of availableLanguages | sort" [value]="code">
-                    {{ code | uppercase }} ({{ code | localeLanguageName }})
-                </option>
-            </select>
-        </vdr-form-field>
-        <vdr-form-field [label]="'common.locale' | translate">
-            <ng-select
-                appendTo="body"
-                [items]="availableLocales"
-                [(ngModel)]="currentLocale"
-                (ngModelChange)="updatePreviewLocale()"
-                [placeholder]="'common.browser-default' | translate"
-            >
-                <ng-template ng-label-tmp let-item="item" let-clear="clear">
-                    {{ item }} ({{ item | localeRegionName }})
-                </ng-template>
-                <ng-template ng-option-tmp let-item="item">
-                    {{ item }} ({{ item | localeRegionName }})
-                </ng-template>
-            </ng-select>
-        </vdr-form-field>
-    </div>
-</div>
-<div class="card">
-    <div class="card-header">
-        <span class="pr-1">{{ 'common.sample-formatting' | translate }}:</span
-        ><strong>{{ previewLocale | localeLanguageName : previewLocale }}</strong>
-    </div>
-    <div class="card-block">
-        <div class="clr-row">
-            <div class="clr-col-sm-4">
-                <vdr-labeled-data [label]="'common.medium-date' | translate">
-                    {{ now | localeDate : 'medium' : previewLocale }}
-                </vdr-labeled-data>
-                <vdr-labeled-data [label]="'common.short-date' | translate">
-                    {{ now | localeDate : 'short' : previewLocale }}
-                </vdr-labeled-data>
-            </div>
-            <div class="clr-col-sm-4">
-                <select name="currency" class="currency" [(ngModel)]="selectedCurrencyCode">
-                    <option *ngFor="let code of availableCurrencyCodes | sort" [value]="code">
-                        {{ code | uppercase }} ({{ code | localeCurrencyName : 'full' : previewLocale }})
+
+<ng-container *ngIf="isLoading">
+    <div class="progress loop"></div>
+</ng-container>
+
+<ng-container *ngIf="!isLoading">
+    <div class="clr-row">
+        <div class="flex pl-2 mb-2">
+            <vdr-form-field [label]="'common.language' | translate" class="mr-2">
+                <select name="options" [(ngModel)]="currentLanguage" (ngModelChange)="updatePreviewLocale()">
+                    <option *ngFor="let code of availableLanguages | sort" [value]="code">
+                        {{ code | uppercase }} ({{ code | localeLanguageName }})
                     </option>
                 </select>
-            </div>
-            <div class="clr-col-sm-4">
-                <vdr-labeled-data [label]="'common.price' | translate">
-                    {{ 12345 | localeCurrency : selectedCurrencyCode : previewLocale }}
-                </vdr-labeled-data>
+            </vdr-form-field>
+            <vdr-form-field [label]="'common.locale' | translate">
+                <ng-select appendTo="body" [items]="availableLocales" [(ngModel)]="currentLocale"
+                    (ngModelChange)="updatePreviewLocale()" [placeholder]="'common.browser-default' | translate">
+                    <ng-template ng-label-tmp let-item="item" let-clear="clear">
+                        {{ item }} ({{ item | localeRegionName }})
+                    </ng-template>
+                    <ng-template ng-option-tmp let-item="item">
+                        {{ item }} ({{ item | localeRegionName }})
+                    </ng-template>
+                </ng-select>
+            </vdr-form-field>
+        </div>
+    </div>
+    <div class="card">
+        <div class="card-header">
+            <span class="pr-1">{{ 'common.sample-formatting' | translate }}:</span><strong>{{ previewLocale |
+                localeLanguageName : previewLocale }}</strong>
+        </div>
+        <div class="card-block">
+            <div class="clr-row">
+                <div class="clr-col-sm-4">
+                    <vdr-labeled-data [label]="'common.medium-date' | translate">
+                        {{ now | localeDate : 'medium' : previewLocale }}
+                    </vdr-labeled-data>
+                    <vdr-labeled-data [label]="'common.short-date' | translate">
+                        {{ now | localeDate : 'short' : previewLocale }}
+                    </vdr-labeled-data>
+                </div>
+                <div class="clr-col-sm-4">
+                    <select name="currency" class="currency" [(ngModel)]="selectedCurrencyCode">
+                        <option *ngFor="let code of availableCurrencyCodes | sort" [value]="code">
+                            {{ code | uppercase }} ({{ code | localeCurrencyName : 'full' : previewLocale }})
+                        </option>
+                    </select>
+                </div>
+                <div class="clr-col-sm-4">
+                    <vdr-labeled-data [label]="'common.price' | translate">
+                        {{ 12345 | localeCurrency : selectedCurrencyCode : previewLocale }}
+                    </vdr-labeled-data>
+                </div>
             </div>
         </div>
     </div>
-</div>
+</ng-container>
+
 <ng-template vdrDialogButtons>
     <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
     <button type="submit" (click)="setLanguage()" class="btn btn-primary">
         {{ 'common.set-language' | translate }}
     </button>
-</ng-template>
+</ng-template>

+ 33 - 256
packages/admin-ui/src/lib/core/src/components/ui-language-switcher-dialog/ui-language-switcher-dialog.component.ts

@@ -1,7 +1,10 @@
-import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
 
+import { Subject, finalize, take, takeUntil } from 'rxjs';
 import { CurrencyCode, LanguageCode } from '../../common/generated-types';
 import { Dialog } from '../../providers/modal/modal.types';
+import { DataService } from '../../data/providers/data.service';
+import { getAppConfig } from '../../app.config';
 
 @Component({
     selector: 'vdr-ui-language-switcher',
@@ -9,275 +12,49 @@ import { Dialog } from '../../providers/modal/modal.types';
     styleUrls: ['./ui-language-switcher-dialog.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class UiLanguageSwitcherDialogComponent implements Dialog<[LanguageCode, string | undefined]>, OnInit {
+export class UiLanguageSwitcherDialogComponent
+    implements Dialog<[LanguageCode, string | undefined]>, OnInit, OnDestroy
+{
+    isLoading = true;
+    private destroy$ = new Subject<void>();
     resolveWith: (result?: [LanguageCode, string | undefined]) => void;
     currentLanguage: LanguageCode;
     availableLanguages: LanguageCode[] = [];
     currentLocale: string | undefined;
-    availableLocales: string[] = [
-        'AF',
-        'AL',
-        'DZ',
-        'AS',
-        'AD',
-        'AO',
-        'AI',
-        'AQ',
-        'AG',
-        'AR',
-        'AM',
-        'AW',
-        'AU',
-        'AT',
-        'AZ',
-        'BS',
-        'BH',
-        'BD',
-        'BB',
-        'BY',
-        'BE',
-        'BZ',
-        'BJ',
-        'BM',
-        'BT',
-        'BO',
-        'BQ',
-        'BA',
-        'BW',
-        'BV',
-        'BR',
-        'IO',
-        'BN',
-        'BG',
-        'BF',
-        'BI',
-        'CV',
-        'KH',
-        'CM',
-        'CA',
-        'KY',
-        'CF',
-        'TD',
-        'CL',
-        'CN',
-        'CX',
-        'CC',
-        'CO',
-        'KM',
-        'CD',
-        'CG',
-        'CK',
-        'CR',
-        'HR',
-        'CU',
-        'CW',
-        'CY',
-        'CZ',
-        'CI',
-        'DK',
-        'DJ',
-        'DM',
-        'DO',
-        'EC',
-        'EG',
-        'SV',
-        'GQ',
-        'ER',
-        'EE',
-        'SZ',
-        'ET',
-        'FK',
-        'FO',
-        'FJ',
-        'FI',
-        'FR',
-        'GF',
-        'PF',
-        'TF',
-        'GA',
-        'GM',
-        'GE',
-        'DE',
-        'GH',
-        'GI',
-        'GR',
-        'GL',
-        'GD',
-        'GP',
-        'GU',
-        'GT',
-        'GG',
-        'GN',
-        'GW',
-        'GY',
-        'HT',
-        'HM',
-        'VA',
-        'HN',
-        'HK',
-        'HU',
-        'IS',
-        'IN',
-        'ID',
-        'IR',
-        'IQ',
-        'IE',
-        'IM',
-        'IL',
-        'IT',
-        'JM',
-        'JP',
-        'JE',
-        'JO',
-        'KZ',
-        'KE',
-        'KI',
-        'KP',
-        'KR',
-        'KW',
-        'KG',
-        'LA',
-        'LV',
-        'LB',
-        'LS',
-        'LR',
-        'LY',
-        'LI',
-        'LT',
-        'LU',
-        'MO',
-        'MG',
-        'MW',
-        'MY',
-        'MV',
-        'ML',
-        'MT',
-        'MH',
-        'MQ',
-        'MR',
-        'MU',
-        'YT',
-        'MX',
-        'FM',
-        'MD',
-        'MC',
-        'MN',
-        'ME',
-        'MS',
-        'MA',
-        'MZ',
-        'MM',
-        'NA',
-        'NR',
-        'NP',
-        'NL',
-        'NC',
-        'NZ',
-        'NI',
-        'NE',
-        'NG',
-        'NU',
-        'NF',
-        'MK',
-        'MP',
-        'NO',
-        'OM',
-        'PK',
-        'PW',
-        'PS',
-        'PA',
-        'PG',
-        'PY',
-        'PE',
-        'PH',
-        'PN',
-        'PL',
-        'PT',
-        'PR',
-        'QA',
-        'RO',
-        'RU',
-        'RW',
-        'RE',
-        'BL',
-        'SH',
-        'KN',
-        'LC',
-        'MF',
-        'PM',
-        'VC',
-        'WS',
-        'SM',
-        'ST',
-        'SA',
-        'SN',
-        'RS',
-        'SC',
-        'SL',
-        'SG',
-        'SX',
-        'SK',
-        'SI',
-        'SB',
-        'SO',
-        'ZA',
-        'GS',
-        'SS',
-        'ES',
-        'LK',
-        'SD',
-        'SR',
-        'SJ',
-        'SE',
-        'CH',
-        'SY',
-        'TW',
-        'TJ',
-        'TZ',
-        'TH',
-        'TL',
-        'TG',
-        'TK',
-        'TO',
-        'TT',
-        'TN',
-        'TR',
-        'TM',
-        'TC',
-        'TV',
-        'UG',
-        'UA',
-        'AE',
-        'GB',
-        'UM',
-        'US',
-        'UY',
-        'UZ',
-        'VU',
-        'VE',
-        'VN',
-        'VG',
-        'VI',
-        'WF',
-        'EH',
-        'YE',
-        'ZM',
-        'ZW',
-        'AX',
-    ];
+    availableLocales: string[] = [];
     availableCurrencyCodes = Object.values(CurrencyCode);
-    selectedCurrencyCode = 'USD';
+    selectedCurrencyCode: string;
     previewLocale: string;
     readonly browserDefaultLocale: string | undefined;
     readonly now = new Date().toISOString();
 
-    constructor() {
+    constructor(private dataService: DataService, private changeDetector: ChangeDetectorRef) {
         const browserLanguage = navigator.language.split('-');
         this.browserDefaultLocale = browserLanguage.length === 1 ? undefined : browserLanguage[1];
     }
 
     ngOnInit() {
         this.updatePreviewLocale();
+
+        this.dataService.settings
+            .getActiveChannel()
+            .mapStream(data => data.activeChannel.defaultCurrencyCode)
+            .pipe(
+                take(1),
+                takeUntil(this.destroy$),
+                finalize(() => {
+                    this.isLoading = false;
+                    this.changeDetector.markForCheck();
+                }),
+            )
+            .subscribe(x => {
+                this.selectedCurrencyCode = x;
+            });
+    }
+
+    ngOnDestroy(): void {
+        this.destroy$.next();
+        this.destroy$.complete();
     }
 
     updatePreviewLocale() {
@@ -294,7 +71,7 @@ export class UiLanguageSwitcherDialogComponent implements Dialog<[LanguageCode,
         this.resolveWith();
     }
 
-    private createLocaleString(languageCode: LanguageCode, region?: string): string {
+    private createLocaleString(languageCode: LanguageCode, region?: string | null): string {
         if (!region) {
             return languageCode;
         }

+ 19 - 4
packages/admin-ui/src/lib/core/src/core.module.ts

@@ -7,7 +7,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import { TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-translate/core';
 
 import { getAppConfig } from './app.config';
-import { getDefaultUiLanguage } from './common/utilities/get-default-ui-language';
+import { getDefaultUiLanguage, getDefaultUiLocale } from './common/utilities/get-default-ui-language';
 import { AlertsComponent } from './components/alerts/alerts.component';
 import { AppShellComponent } from './components/app-shell/app-shell.component';
 import { BaseNavComponent } from './components/base-nav/base-nav.component';
@@ -74,29 +74,44 @@ export class CoreModule {
         private dataService: DataService,
         private notificationService: NotificationService,
     ) {
-        this.initUiLanguages();
+        this.initUiLanguagesAndLocales();
         this.initUiTitle();
         this.initAlerts();
     }
 
-    private initUiLanguages() {
+    private initUiLanguagesAndLocales() {
         const defaultLanguage = getDefaultUiLanguage();
+        const defaultLocale = getDefaultUiLocale();
+
         const lastLanguage = this.localStorageService.get('uiLanguageCode');
         const availableLanguages = getAppConfig().availableLanguages;
+        const availableLocales = getAppConfig().availableLocales;
 
-        if (!availableLanguages.includes(defaultLanguage)) {
+        if (!!defaultLanguage && !availableLanguages.includes(defaultLanguage)) {
             throw new Error(
                 `The defaultLanguage "${defaultLanguage}" must be one of the availableLanguages [${availableLanguages
                     .map(l => `"${l}"`)
                     .join(', ')}]`,
             );
         }
+
+        if (!!defaultLocale && !availableLocales.includes(defaultLocale)) {
+            throw new Error(
+                `The defaultLocale "${defaultLocale}" must be one of the availableLocales [${availableLocales
+                    .map(l => `"${l}"`)
+                    .join(', ')}]`,
+            );
+        }
+
         const uiLanguage =
             lastLanguage && availableLanguages.includes(lastLanguage) ? lastLanguage : defaultLanguage;
+
         this.localStorageService.set('uiLanguageCode', uiLanguage);
+
         this.i18nService.setLanguage(uiLanguage);
         this.i18nService.setDefaultLanguage(defaultLanguage);
         this.i18nService.setAvailableLanguages(availableLanguages || [defaultLanguage]);
+        this.i18nService.setAvailableLocales(availableLocales || [defaultLocale]);
     }
 
     private initUiTitle() {

+ 4 - 1
packages/admin-ui/src/lib/core/src/providers/i18n/i18n.service.mock.ts

@@ -20,10 +20,13 @@ export class MockI18nService implements MockOf<I18nService> {
 
     isRTL(): boolean {
         return false;
-      }
+    }
 
     availableLanguages: LanguageCode[];
+    availableLocales: string[] = [];
+    setAvailableLocales: (locales: string[]) => void;
     setAvailableLanguages: (languages: LanguageCode[]) => void;
     _availableLanguages: LanguageCode[];
+    _availableLocales: string[] = [];
     ngxTranslate: TranslateService;
 }

+ 12 - 0
packages/admin-ui/src/lib/core/src/providers/i18n/i18n.service.ts

@@ -9,12 +9,17 @@ import { LanguageCode } from '../../common/generated-types';
     providedIn: 'root',
 })
 export class I18nService {
+    _availableLocales: string[] = [];
     _availableLanguages: LanguageCode[] = [];
 
     get availableLanguages(): LanguageCode[] {
         return [...this._availableLanguages];
     }
 
+    get availableLocales(): string[] {
+        return [...this._availableLocales];
+    }
+
     constructor(private ngxTranslate: TranslateService, @Inject(DOCUMENT) private document: Document) {}
 
     /**
@@ -41,6 +46,13 @@ export class I18nService {
         this._availableLanguages = languages;
     }
 
+    /**
+     * Set the available UI locales
+     */
+    setAvailableLocales(locales: string[]) {
+        this._availableLocales = locales;
+    }
+
     /**
      * Translate the given key.
      */

+ 5 - 3
packages/admin-ui/src/lib/core/src/providers/localization/localization.service.ts

@@ -21,9 +21,11 @@ export class LocalizationService {
     direction$: LocalizationDirectionType;
 
     constructor(private i18nService: I18nService, private dataService: DataService) {
-        this.uiLanguageAndLocale$ = this.dataService.client
-            ?.uiState()
-            ?.stream$?.pipe(map(({ uiState }) => [uiState.language, uiState.locale ?? undefined]));
+        this.uiLanguageAndLocale$ = this.dataService.client?.uiState()?.stream$?.pipe(
+            map(({ uiState }) => {
+                return [uiState.language, uiState.locale ?? undefined];
+            }),
+        );
 
         this.direction$ = this.uiLanguageAndLocale$?.pipe(
             map(([languageCode]) => {

+ 1 - 1
packages/admin-ui/src/lib/static/i18n-messages/fa.json

@@ -174,7 +174,7 @@
     "stock-levels": "سطح انبار",
     "stock-location": "انبار",
     "stock-locations": "انبارها",
-    "stock-on-hand": "انبار",
+    "stock-on-hand": "موجودی انبار",
     "tax-category": "دسته بندی مالیاتی",
     "taxes": "مالیات ها",
     "track-inventory": "فروش براساس موجودی انبار",

+ 260 - 5
packages/admin-ui/src/lib/static/vendure-ui-config.json

@@ -6,15 +6,16 @@
     "authTokenHeaderKey": "vendure-auth-token",
     "channelTokenKey": "vendure-token",
     "defaultLanguage": "en",
+    "defaultLocale": "US",
     "availableLanguages": [
         "he",
         "ar",
+        "de",
         "en",
         "es",
-        "zh_Hant",
-        "zh_Hans",
         "pl",
-        "de",
+        "zh_Hans",
+        "zh_Hant",
         "pt_BR",
         "pt_PT",
         "cs",
@@ -26,8 +27,262 @@
         "ne",
         "hr"
     ],
+    "availableLocales": [
+        "AF",
+        "AL",
+        "DZ",
+        "AS",
+        "AD",
+        "AO",
+        "AI",
+        "AQ",
+        "AG",
+        "AR",
+        "AM",
+        "AW",
+        "AU",
+        "AT",
+        "AZ",
+        "BS",
+        "BH",
+        "BD",
+        "BB",
+        "BY",
+        "BE",
+        "BZ",
+        "BJ",
+        "BM",
+        "BT",
+        "BO",
+        "BQ",
+        "BA",
+        "BW",
+        "BV",
+        "BR",
+        "IO",
+        "BN",
+        "BG",
+        "BF",
+        "BI",
+        "CV",
+        "KH",
+        "CM",
+        "CA",
+        "KY",
+        "CF",
+        "TD",
+        "CL",
+        "CN",
+        "CX",
+        "CC",
+        "CO",
+        "KM",
+        "CD",
+        "CG",
+        "CK",
+        "CR",
+        "HR",
+        "CU",
+        "CW",
+        "CY",
+        "CZ",
+        "CI",
+        "DK",
+        "DJ",
+        "DM",
+        "DO",
+        "EC",
+        "EG",
+        "SV",
+        "GQ",
+        "ER",
+        "EE",
+        "SZ",
+        "ET",
+        "FK",
+        "FO",
+        "FJ",
+        "FI",
+        "FR",
+        "GF",
+        "PF",
+        "TF",
+        "GA",
+        "GM",
+        "GE",
+        "DE",
+        "GH",
+        "GI",
+        "GR",
+        "GL",
+        "GD",
+        "GP",
+        "GU",
+        "GT",
+        "GG",
+        "GN",
+        "GW",
+        "GY",
+        "HT",
+        "HM",
+        "VA",
+        "HN",
+        "HK",
+        "HU",
+        "IS",
+        "IN",
+        "ID",
+        "IR",
+        "IQ",
+        "IE",
+        "IM",
+        "IL",
+        "IT",
+        "JM",
+        "JP",
+        "JE",
+        "JO",
+        "KZ",
+        "KE",
+        "KI",
+        "KP",
+        "KR",
+        "KW",
+        "KG",
+        "LA",
+        "LV",
+        "LB",
+        "LS",
+        "LR",
+        "LY",
+        "LI",
+        "LT",
+        "LU",
+        "MO",
+        "MG",
+        "MW",
+        "MY",
+        "MV",
+        "ML",
+        "MT",
+        "MH",
+        "MQ",
+        "MR",
+        "MU",
+        "YT",
+        "MX",
+        "FM",
+        "MD",
+        "MC",
+        "MN",
+        "ME",
+        "MS",
+        "MA",
+        "MZ",
+        "MM",
+        "NA",
+        "NR",
+        "NP",
+        "NL",
+        "NC",
+        "NZ",
+        "NI",
+        "NE",
+        "NG",
+        "NU",
+        "NF",
+        "MK",
+        "MP",
+        "NO",
+        "OM",
+        "PK",
+        "PW",
+        "PS",
+        "PA",
+        "PG",
+        "PY",
+        "PE",
+        "PH",
+        "PN",
+        "PL",
+        "PT",
+        "PR",
+        "QA",
+        "RO",
+        "RU",
+        "RW",
+        "RE",
+        "BL",
+        "SH",
+        "KN",
+        "LC",
+        "MF",
+        "PM",
+        "VC",
+        "WS",
+        "SM",
+        "ST",
+        "SA",
+        "SN",
+        "RS",
+        "SC",
+        "SL",
+        "SG",
+        "SX",
+        "SK",
+        "SI",
+        "SB",
+        "SO",
+        "ZA",
+        "GS",
+        "SS",
+        "ES",
+        "LK",
+        "SD",
+        "SR",
+        "SJ",
+        "SE",
+        "CH",
+        "SY",
+        "TW",
+        "TJ",
+        "TZ",
+        "TH",
+        "TL",
+        "TG",
+        "TK",
+        "TO",
+        "TT",
+        "TN",
+        "TR",
+        "TM",
+        "TC",
+        "TV",
+        "UG",
+        "UA",
+        "AE",
+        "GB",
+        "UM",
+        "US",
+        "UY",
+        "UZ",
+        "VU",
+        "VE",
+        "VN",
+        "VG",
+        "VI",
+        "WF",
+        "EH",
+        "YE",
+        "ZM",
+        "ZW",
+        "AX"
+    ],
     "brand": "",
     "hideVendureBranding": false,
     "hideVersion": false,
-    "cancellationReasons": ["order.cancel-reason-customer-request", "order.cancel-reason-not-available"]
-}
+    "cancellationReasons": [
+        "order.cancel-reason-customer-request",
+        "order.cancel-reason-not-available"
+    ]
+}

+ 10 - 1
packages/common/src/shared-types.ts

@@ -34,6 +34,7 @@ export type DeepRequired<T, U extends object | undefined = undefined> = T extend
 /**
  * A type representing the type rather than instance of a class.
  */
+// eslint-disable-next-line @typescript-eslint/ban-types
 export interface Type<T> extends Function {
     // eslint-disable-next-line @typescript-eslint/prefer-function-type
     new (...args: any[]): T;
@@ -273,9 +274,12 @@ export interface AdminUiConfig {
     /**
      * @description
      * The default locale for the Admin UI. The locale affects the formatting of
-     * currencies & dates.
+     * currencies & dates. Must be one of the items specified
+     * in the `availableLocales` property.
      *
      * If not set, the browser default locale will be used.
+     *
+     * @since 2.2.0
      */
     defaultLocale?: string;
     /**
@@ -283,6 +287,11 @@ export interface AdminUiConfig {
      * An array of languages for which translations exist for the Admin UI.
      */
     availableLanguages: LanguageCode[];
+    /**
+     * @description
+     * An array of locales to be used on Admin UI.
+     */
+    availableLocales: string[];
     /**
      * @description
      * If you are using an external {@link AuthenticationStrategy} for the Admin API, you can configure