Browse Source

feat(admin-ui): Support for language regions (language + locale)

Relates to #1196
Michael Bromley 4 năm trước cách đây
mục cha
commit
b5cdbce581
47 tập tin đã thay đổi với 706 bổ sung118 xóa
  1. 27 27
      packages/admin-ui/i18n-coverage.json
  2. 27 4
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  3. 8 0
      packages/admin-ui/src/lib/core/src/common/utilities/get-default-ui-language.ts
  4. 1 1
      packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.html
  5. 12 6
      packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.ts
  6. 73 6
      packages/admin-ui/src/lib/core/src/components/ui-language-switcher-dialog/ui-language-switcher-dialog.component.html
  7. 7 0
      packages/admin-ui/src/lib/core/src/components/ui-language-switcher-dialog/ui-language-switcher-dialog.component.scss
  8. 289 6
      packages/admin-ui/src/lib/core/src/components/ui-language-switcher-dialog/ui-language-switcher-dialog.component.ts
  9. 1 1
      packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.html
  10. 1 1
      packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.ts
  11. 3 1
      packages/admin-ui/src/lib/core/src/data/client-state/client-defaults.ts
  12. 26 36
      packages/admin-ui/src/lib/core/src/data/client-state/client-resolvers.ts
  13. 2 0
      packages/admin-ui/src/lib/core/src/data/client-state/client-types.graphql
  14. 11 2
      packages/admin-ui/src/lib/core/src/data/definitions/client-definitions.ts
  15. 16 4
      packages/admin-ui/src/lib/core/src/data/providers/client-data.service.ts
  16. 1 0
      packages/admin-ui/src/lib/core/src/providers/local-storage/local-storage.service.ts
  17. 1 0
      packages/admin-ui/src/lib/core/src/public_api.ts
  18. 6 3
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-base.pipe.ts
  19. 6 0
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-language-name.pipe.spec.ts
  20. 30 0
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-region-name.pipe.spec.ts
  21. 46 0
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-region-name.pipe.ts
  22. 2 0
      packages/admin-ui/src/lib/core/src/shared/shared.module.ts
  23. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  24. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  25. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  26. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  27. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  28. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/it.json
  29. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  30. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  31. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  32. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/ru.json
  33. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/uk.json
  34. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  35. 6 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  36. 4 1
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  37. 1 1
      packages/common/src/generated-shop-types.ts
  38. 4 1
      packages/common/src/generated-types.ts
  39. 8 0
      packages/common/src/shared-types.ts
  40. 4 1
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  41. 1 1
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  42. 1 12
      packages/dev-server/dev-config.ts
  43. 4 1
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  44. 4 1
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  45. 1 1
      packages/payments-plugin/e2e/graphql/generated-shop-types.ts
  46. 0 0
      schema-admin.json
  47. 0 0
      schema-shop.json

+ 27 - 27
packages/admin-ui/i18n-coverage.json

@@ -1,69 +1,69 @@
 {
-  "generatedOn": "2021-11-26T11:42:42.094Z",
-  "lastCommit": "cee98091eeb0fedc5b5269bdee8a8b237f0c7c8e",
+  "generatedOn": "2021-12-01T14:27:05.793Z",
+  "lastCommit": "8f218daf16959f6c8aa6f1d0262aeb124bd4214b",
   "translationStatus": {
     "cs": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 591,
-      "percentage": 94
+      "percentage": 93
     },
     "de": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 570,
-      "percentage": 91
+      "percentage": 90
     },
     "en": {
-      "tokenCount": 628,
-      "translatedCount": 627,
-      "percentage": 100
+      "tokenCount": 634,
+      "translatedCount": 628,
+      "percentage": 99
     },
     "es": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 623,
-      "percentage": 99
+      "percentage": 98
     },
     "fr": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 613,
-      "percentage": 98
+      "percentage": 97
     },
     "it": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 621,
-      "percentage": 99
+      "percentage": 98
     },
     "pl": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 405,
       "percentage": 64
     },
     "pt_BR": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 588,
-      "percentage": 94
+      "percentage": 93
     },
     "pt_PT": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 622,
-      "percentage": 99
+      "percentage": 98
     },
     "ru": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 621,
-      "percentage": 99
+      "percentage": 98
     },
     "uk": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 621,
-      "percentage": 99
+      "percentage": 98
     },
     "zh_Hans": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 558,
-      "percentage": 89
+      "percentage": 88
     },
     "zh_Hant": {
-      "tokenCount": 628,
+      "tokenCount": 634,
       "translatedCount": 385,
       "percentage": 61
     }

+ 27 - 4
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -2243,7 +2243,10 @@ export type Mutation = {
   addCustomersToGroup: CustomerGroup;
   addFulfillmentToOrder: AddFulfillmentToOrderResult;
   /**
-   * Used to manually create a new Payment against an Order. This is used when a completed Order
+   * Used to manually create a new Payment against an Order.
+   * This can be used by an Administrator when an Order is in the ArrangingPayment state.
+   *
+   * It is also used when a completed Order
    * has been modified (using `modifyOrder`) and the price has increased. The extra payment
    * can then be manually arranged by the administrator, and the details used to create a new
    * Payment.
@@ -2393,6 +2396,7 @@ export type Mutation = {
   setDisplayUiExtensionPoints: Scalars['Boolean'];
   setOrderCustomFields?: Maybe<Order>;
   setUiLanguage: LanguageCode;
+  setUiLocale?: Maybe<Scalars['String']>;
   setUiTheme: Scalars['String'];
   settlePayment: SettlePaymentResult;
   settleRefund: SettleRefundResult;
@@ -2862,6 +2866,11 @@ export type MutationSetUiLanguageArgs = {
 };
 
 
+export type MutationSetUiLocaleArgs = {
+  locale?: Maybe<Scalars['String']>;
+};
+
+
 export type MutationSetUiThemeArgs = {
   theme: Scalars['String'];
 };
@@ -4928,6 +4937,7 @@ export type TransitionPaymentToStateResult = Payment | PaymentStateTransitionErr
 export type UiState = {
   __typename?: 'UiState';
   language: LanguageCode;
+  locale?: Maybe<Scalars['String']>;
   contentLanguage: LanguageCode;
   theme: Scalars['String'];
   displayUiExtensionPoints: Scalars['Boolean'];
@@ -5483,10 +5493,18 @@ export type SetAsLoggedOutMutation = { setAsLoggedOut: (
 
 export type SetUiLanguageMutationVariables = Exact<{
   languageCode: LanguageCode;
+  locale?: Maybe<Scalars['String']>;
+}>;
+
+
+export type SetUiLanguageMutation = Pick<Mutation, 'setUiLanguage' | 'setUiLocale'>;
+
+export type SetUiLocaleMutationVariables = Exact<{
+  locale?: Maybe<Scalars['String']>;
 }>;
 
 
-export type SetUiLanguageMutation = Pick<Mutation, 'setUiLanguage'>;
+export type SetUiLocaleMutation = Pick<Mutation, 'setUiLocale'>;
 
 export type SetDisplayUiExtensionPointsMutationVariables = Exact<{
   display: Scalars['Boolean'];
@@ -5530,7 +5548,7 @@ export type GetUiStateQueryVariables = Exact<{ [key: string]: never; }>;
 
 export type GetUiStateQuery = { uiState: (
     { __typename?: 'UiState' }
-    & Pick<UiState, 'language' | 'contentLanguage' | 'theme' | 'displayUiExtensionPoints'>
+    & Pick<UiState, 'language' | 'locale' | 'contentLanguage' | 'theme' | 'displayUiExtensionPoints'>
   ) };
 
 export type GetClientStateQueryVariables = Exact<{ [key: string]: never; }>;
@@ -5544,7 +5562,7 @@ export type GetClientStateQuery = { networkStatus: (
     & UserStatusFragment
   ), uiState: (
     { __typename?: 'UiState' }
-    & Pick<UiState, 'language' | 'contentLanguage' | 'theme'>
+    & Pick<UiState, 'language' | 'locale' | 'contentLanguage' | 'theme' | 'displayUiExtensionPoints'>
   ) };
 
 export type SetActiveChannelMutationVariables = Exact<{
@@ -9082,6 +9100,11 @@ export namespace SetUiLanguage {
   export type Mutation = SetUiLanguageMutation;
 }
 
+export namespace SetUiLocale {
+  export type Variables = SetUiLocaleMutationVariables;
+  export type Mutation = SetUiLocaleMutation;
+}
+
 export namespace SetDisplayUiExtensionPoints {
   export type Variables = SetDisplayUiExtensionPointsMutationVariables;
   export type Mutation = SetDisplayUiExtensionPointsMutation;

+ 8 - 0
packages/admin-ui/src/lib/core/src/common/utilities/get-default-ui-language.ts

@@ -4,3 +4,11 @@ import { LanguageCode } from '../generated-types';
 export function getDefaultUiLanguage(): LanguageCode {
     return getAppConfig().defaultLanguage;
 }
+
+export function getDefaultUiLocale(): string | undefined {
+    const defaultLocale = getAppConfig().defaultLocale;
+    if (defaultLocale) {
+        return defaultLocale;
+    }
+    return navigator.language.split('-')[1]?.toUpperCase();
+}

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

@@ -7,7 +7,7 @@
         <div class="header-actions">
             <vdr-channel-switcher *vdrIfMultichannel></vdr-channel-switcher>
             <vdr-user-menu [userName]="userName$ | async"
-                           [uiLanguage]="uiLanguage$ | async"
+                           [uiLanguageAndLocale]="uiLanguageAndLocale$ | async"
                            [availableLanguages]="availableLanguages"
                            (selectUiLanguage)="selectUiLanguage()"
                            (logOut)="logOut()"></vdr-user-menu>

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

@@ -19,7 +19,7 @@ import { UiLanguageSwitcherDialogComponent } from '../ui-language-switcher-dialo
 })
 export class AppShellComponent implements OnInit {
     userName$: Observable<string>;
-    uiLanguage$: Observable<LanguageCode>;
+    uiLanguageAndLocale$: Observable<[LanguageCode, string | undefined]>;
     availableLanguages: LanguageCode[] = [];
 
     constructor(
@@ -35,30 +35,36 @@ export class AppShellComponent implements OnInit {
         this.userName$ = this.dataService.client
             .userStatus()
             .single$.pipe(map(data => data.userStatus.username));
-        this.uiLanguage$ = this.dataService.client.uiState().stream$.pipe(map(data => data.uiState.language));
+        this.uiLanguageAndLocale$ = this.dataService.client
+            .uiState()
+            .stream$.pipe(map(({ uiState }) => [uiState.language, uiState.locale ?? undefined]));
         this.availableLanguages = this.i18nService.availableLanguages;
     }
 
     selectUiLanguage() {
-        this.uiLanguage$
+        this.uiLanguageAndLocale$
             .pipe(
                 take(1),
-                switchMap(currentLanguage =>
+                switchMap(([currentLanguage, currentLocale]) =>
                     this.modalService.fromComponent(UiLanguageSwitcherDialogComponent, {
                         closable: true,
-                        size: 'md',
+                        size: 'lg',
                         locals: {
                             availableLanguages: this.availableLanguages,
                             currentLanguage,
+                            currentLocale,
                         },
                     }),
                 ),
-                switchMap(value => (value ? this.dataService.client.setUiLanguage(value) : EMPTY)),
+                switchMap(result =>
+                    result ? this.dataService.client.setUiLanguage(result[0], result[1]) : EMPTY,
+                ),
             )
             .subscribe(result => {
                 if (result.setUiLanguage) {
                     this.i18nService.setLanguage(result.setUiLanguage);
                     this.localStorageService.set('uiLanguageCode', result.setUiLanguage);
+                    this.localStorageService.set('uiLocale', result.setUiLocale ?? undefined);
                 }
             });
     }

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

@@ -1,8 +1,75 @@
 <ng-template vdrDialogTitle>{{ 'common.select-display-language' | translate }}</ng-template>
-
-<div *ngFor="let code of availableLanguages" >
-    <button class="btn btn-link btn-sm" (click)="setLanguage(code)">
-        <clr-icon [attr.shape]="code === currentLanguage ? 'dot-circle' : 'circle'"></clr-icon>
-        {{ code | uppercase }} ({{ code | localeLanguageName }})
-    </button>
+<div class="clr-row">
+    <div class="clr-col-md-6">
+        <clr-select-container>
+            <label>{{ 'common.language' | translate }}</label>
+            <select
+                clrSelect
+                name="options"
+                [(ngModel)]="currentLanguage"
+                (ngModelChange)="updatePreviewLocale()"
+            >
+                <option *ngFor="let code of availableLanguages | sort" [value]="code">
+                    {{ code | uppercase }} ({{ code | localeLanguageName }})
+                </option>
+            </select>
+        </clr-select-container>
+    </div>
+    <div class="clr-col-md-6">
+        <clr-datalist-container>
+            <label>{{ 'common.locale' | translate }}</label>
+            <input
+                clrDatalistInput
+                [(ngModel)]="currentLocale"
+                (ngModelChange)="updatePreviewLocale()"
+                [placeholder]="'common.browser-default' | translate"
+                class="locale"
+                name="Locale"
+            />
+            <datalist>
+                <option *ngFor="let locale of availableLocales" [value]="locale">
+                    {{ locale }} ({{ locale | localeRegionName }})
+                </option>
+            </datalist>
+        </clr-datalist-container>
+    </div>
+</div>
+<div class="card">
+    <div class="card-header">
+        <span class="p2">{{ '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 clrSelect 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>
+<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>

+ 7 - 0
packages/admin-ui/src/lib/core/src/components/ui-language-switcher-dialog/ui-language-switcher-dialog.component.scss

@@ -1,3 +1,10 @@
 :host {
 }
 
+select.currency {
+    max-width: 200px;
+}
+
+input.locale {
+    text-transform: uppercase;
+}

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

@@ -1,6 +1,6 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 
-import { LanguageCode } from '../../common/generated-types';
+import { CurrencyCode, LanguageCode } from '../../common/generated-types';
 import { Dialog } from '../../providers/modal/modal.service';
 
 @Component({
@@ -9,12 +9,295 @@ import { Dialog } from '../../providers/modal/modal.service';
     styleUrls: ['./ui-language-switcher-dialog.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class UiLanguageSwitcherDialogComponent implements Dialog<LanguageCode> {
-    resolveWith: (result?: LanguageCode) => void;
+export class UiLanguageSwitcherDialogComponent implements Dialog<[LanguageCode, string | undefined]>, OnInit {
+    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',
+    ];
+    availableCurrencyCodes = Object.values(CurrencyCode);
+    selectedCurrencyCode = 'USD';
+    previewLocale: string;
+    readonly browserDefaultLocale: string | undefined;
+    readonly now = new Date().toISOString();
 
-    setLanguage(languageCode: LanguageCode) {
-        this.resolveWith(languageCode);
+    constructor() {
+        const browserLanguage = navigator.language.split('-');
+        this.browserDefaultLocale = browserLanguage.length === 1 ? undefined : browserLanguage[1];
+    }
+
+    ngOnInit() {
+        this.updatePreviewLocale();
+    }
+
+    updatePreviewLocale() {
+        if (!this.currentLocale || this.currentLocale.length === 0 || this.currentLocale.length === 2) {
+            this.previewLocale = this.createLocaleString(this.currentLanguage, this.currentLocale);
+        }
+    }
+
+    setLanguage() {
+        this.resolveWith([this.currentLanguage, this.currentLocale?.toUpperCase()]);
+    }
+
+    cancel() {
+        this.resolveWith();
+    }
+
+    private createLocaleString(languageCode: LanguageCode, region?: string): string {
+        if (!region) {
+            return languageCode;
+        }
+        return [languageCode, region.toUpperCase()].join('-');
     }
 }

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

@@ -15,7 +15,7 @@
                 (click)="selectUiLanguage.emit()"
                 [title]="'common.select-display-language' | translate"
             >
-                <clr-icon shape="language"></clr-icon> {{ uiLanguage | localeLanguageName }}
+                <clr-icon shape="language"></clr-icon> {{ uiLanguageAndLocale?.[0] | localeLanguageName }}
             </button>
         </ng-container>
         <div class="dropdown-item">

+ 1 - 1
packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.ts

@@ -10,7 +10,7 @@ import { LanguageCode } from '../../common/generated-types';
 export class UserMenuComponent {
     @Input() userName = '';
     @Input() availableLanguages: LanguageCode[] = [];
-    @Input() uiLanguage: LanguageCode;
+    @Input() uiLanguageAndLocale: [LanguageCode, string | undefined];
     @Output() logOut = new EventEmitter<void>();
     @Output() selectUiLanguage = new EventEmitter<void>();
 }

+ 3 - 1
packages/admin-ui/src/lib/core/src/data/client-state/client-defaults.ts

@@ -1,10 +1,11 @@
 import { getAppConfig } from '../../app.config';
 import { GetNetworkStatus, GetUiState, GetUserStatus } from '../../common/generated-types';
-import { getDefaultUiLanguage } from '../../common/utilities/get-default-ui-language';
+import { getDefaultUiLanguage, getDefaultUiLocale } from '../../common/utilities/get-default-ui-language';
 import { LocalStorageService } from '../../providers/local-storage/local-storage.service';
 
 export function getClientDefaults(localStorageService: LocalStorageService) {
     const currentLanguage = localStorageService.get('uiLanguageCode') || getDefaultUiLanguage();
+    const currentLocale = localStorageService.get('uiLocale') || getDefaultUiLocale();
     const currentContentLanguage = localStorageService.get('contentLanguageCode') || getDefaultUiLanguage();
     const activeTheme = localStorageService.get('activeTheme') || 'default';
     return {
@@ -23,6 +24,7 @@ export function getClientDefaults(localStorageService: LocalStorageService) {
         } as GetUserStatus.UserStatus,
         uiState: {
             language: currentLanguage,
+            locale: currentLocale || '',
             contentLanguage: currentContentLanguage,
             theme: activeTheme,
             displayUiExtensionPoints: false,

+ 26 - 36
packages/admin-ui/src/lib/core/src/data/client-state/client-resolvers.ts

@@ -10,6 +10,7 @@ import {
     SetContentLanguage,
     SetDisplayUiExtensionPoints,
     SetUiLanguage,
+    SetUiLocale,
     SetUiTheme,
     UpdateUserChannels,
     UserStatus,
@@ -74,60 +75,35 @@ export const clientResolvers: ResolverDefinition = {
         setUiLanguage: (_, args: SetUiLanguage.Variables, { cache }): LanguageCode => {
             // tslint:disable-next-line:no-non-null-assertion
             const previous = cache.readQuery<GetUiState.Query>({ query: GET_UI_STATE })!;
-            const data: GetUiState.Query = {
-                uiState: {
-                    __typename: 'UiState',
-                    language: args.languageCode,
-                    contentLanguage: previous.uiState.contentLanguage,
-                    theme: previous.uiState.theme,
-                    displayUiExtensionPoints: previous.uiState.displayUiExtensionPoints,
-                },
-            };
+            const data = updateUiState(previous, 'language', args.languageCode);
             cache.writeQuery({ query: GET_UI_STATE, data });
             return args.languageCode;
         },
+        setUiLocale: (_, args: SetUiLocale.Variables, { cache }): string | undefined => {
+            // tslint:disable-next-line:no-non-null-assertion
+            const previous = cache.readQuery<GetUiState.Query>({ query: GET_UI_STATE })!;
+            const data = updateUiState(previous, 'locale', args.locale);
+            cache.writeQuery({ query: GET_UI_STATE, data });
+            return args.locale ?? undefined;
+        },
         setContentLanguage: (_, args: SetContentLanguage.Variables, { cache }): LanguageCode => {
             // tslint:disable-next-line:no-non-null-assertion
             const previous = cache.readQuery<GetUiState.Query>({ query: GET_UI_STATE })!;
-            const data: GetUiState.Query = {
-                uiState: {
-                    __typename: 'UiState',
-                    language: previous.uiState.language,
-                    contentLanguage: args.languageCode,
-                    theme: previous.uiState.theme,
-                    displayUiExtensionPoints: previous.uiState.displayUiExtensionPoints,
-                },
-            };
+            const data = updateUiState(previous, 'contentLanguage', args.languageCode);
             cache.writeQuery({ query: GET_UI_STATE, data });
             return args.languageCode;
         },
         setUiTheme: (_, args: SetUiTheme.Variables, { cache }): string => {
             // tslint:disable-next-line:no-non-null-assertion
             const previous = cache.readQuery<GetUiState.Query>({ query: GET_UI_STATE })!;
-            const data: GetUiState.Query = {
-                uiState: {
-                    __typename: 'UiState',
-                    language: previous.uiState.language,
-                    contentLanguage: previous.uiState.contentLanguage,
-                    theme: args.theme,
-                    displayUiExtensionPoints: previous.uiState.displayUiExtensionPoints,
-                },
-            };
+            const data = updateUiState(previous, 'theme', args.theme);
             cache.writeQuery({ query: GET_UI_STATE, data });
             return args.theme;
         },
         setDisplayUiExtensionPoints: (_, args: SetDisplayUiExtensionPoints.Variables, { cache }): boolean => {
             // tslint:disable-next-line:no-non-null-assertion
             const previous = cache.readQuery<GetUiState.Query>({ query: GET_UI_STATE })!;
-            const data: GetUiState.Query = {
-                uiState: {
-                    __typename: 'UiState',
-                    language: previous.uiState.language,
-                    contentLanguage: previous.uiState.contentLanguage,
-                    displayUiExtensionPoints: args.display,
-                    theme: previous.uiState.theme,
-                },
-            };
+            const data = updateUiState(previous, 'displayUiExtensionPoints', args.display);
             cache.writeQuery({ query: GET_UI_STATE, data });
             return args.display;
         },
@@ -164,6 +140,20 @@ export const clientResolvers: ResolverDefinition = {
     },
 };
 
+function updateUiState<K extends keyof GetUiState.Query['uiState']>(
+    previous: GetUiState.Query,
+    key: K,
+    value: GetUiState.Query['uiState'][K],
+): GetUiState.Query {
+    return {
+        uiState: {
+            ...previous.uiState,
+            [key]: value,
+            __typename: 'UiState',
+        },
+    };
+}
+
 function updateRequestsInFlight(cache: InMemoryCache, increment: 1 | -1): number {
     const previous = cache.readQuery<GetNetworkStatus.Query>({ query: GET_NEWTORK_STATUS });
     const inFlightRequests = previous ? previous.networkStatus.inFlightRequests + increment : increment;

+ 2 - 0
packages/admin-ui/src/lib/core/src/data/client-state/client-types.graphql

@@ -10,6 +10,7 @@ type Mutation {
     setAsLoggedIn(input: UserStatusInput!): UserStatus!
     setAsLoggedOut: UserStatus!
     setUiLanguage(languageCode: LanguageCode!): LanguageCode!
+    setUiLocale(locale: String): String
     setContentLanguage(languageCode: LanguageCode!): LanguageCode!
     setUiTheme(theme: String!): String!
     setActiveChannel(channelId: ID!): UserStatus!
@@ -32,6 +33,7 @@ type UserStatus {
 
 type UiState {
     language: LanguageCode!
+    locale: String
     contentLanguage: LanguageCode!
     theme: String!
     displayUiExtensionPoints: Boolean!

+ 11 - 2
packages/admin-ui/src/lib/core/src/data/definitions/client-definitions.ts

@@ -46,9 +46,16 @@ export const SET_AS_LOGGED_OUT = gql`
     ${USER_STATUS_FRAGMENT}
 `;
 
-export const SET_UI_LANGUAGE = gql`
-    mutation SetUiLanguage($languageCode: LanguageCode!) {
+export const SET_UI_LANGUAGE_AND_LOCALE = gql`
+    mutation SetUiLanguage($languageCode: LanguageCode!, $locale: String) {
         setUiLanguage(languageCode: $languageCode) @client
+        setUiLocale(locale: $locale) @client
+    }
+`;
+
+export const SET_UI_LOCALE = gql`
+    mutation SetUiLocale($locale: String) {
+        setUiLocale(locale: $locale) @client
     }
 `;
 
@@ -91,6 +98,7 @@ export const GET_UI_STATE = gql`
     query GetUiState {
         uiState @client {
             language
+            locale
             contentLanguage
             theme
             displayUiExtensionPoints
@@ -108,6 +116,7 @@ export const GET_CLIENT_STATE = gql`
         }
         uiState @client {
             language
+            locale
             contentLanguage
             theme
             displayUiExtensionPoints

+ 16 - 4
packages/admin-ui/src/lib/core/src/data/providers/client-data.service.ts

@@ -12,6 +12,7 @@ import {
     SetContentLanguage,
     SetDisplayUiExtensionPoints,
     SetUiLanguage,
+    SetUiLocale,
     SetUiTheme,
     UpdateUserChannels,
 } from '../../common/generated-types';
@@ -26,7 +27,8 @@ import {
     SET_AS_LOGGED_OUT,
     SET_CONTENT_LANGUAGE,
     SET_DISPLAY_UI_EXTENSION_POINTS,
-    SET_UI_LANGUAGE,
+    SET_UI_LANGUAGE_AND_LOCALE,
+    SET_UI_LOCALE,
     SET_UI_THEME,
     UPDATE_USER_CHANNELS,
 } from '../definitions/client-definitions';
@@ -78,9 +80,19 @@ export class ClientDataService {
         return this.baseDataService.query<GetUiState.Query>(GET_UI_STATE, {}, 'cache-first');
     }
 
-    setUiLanguage(languageCode: LanguageCode) {
-        return this.baseDataService.mutate<SetUiLanguage.Mutation, SetUiLanguage.Variables>(SET_UI_LANGUAGE, {
-            languageCode,
+    setUiLanguage(languageCode: LanguageCode, locale?: string) {
+        return this.baseDataService.mutate<SetUiLanguage.Mutation, SetUiLanguage.Variables>(
+            SET_UI_LANGUAGE_AND_LOCALE,
+            {
+                languageCode,
+                locale,
+            },
+        );
+    }
+
+    setUiLocale(locale: string | undefined) {
+        return this.baseDataService.mutate<SetUiLocale.Mutation, SetUiLocale.Variables>(SET_UI_LOCALE, {
+            locale,
         });
     }
 

+ 1 - 0
packages/admin-ui/src/lib/core/src/providers/local-storage/local-storage.service.ts

@@ -8,6 +8,7 @@ export type LocalStorageTypeMap = {
     activeChannelToken: string;
     authToken: string;
     uiLanguageCode: LanguageCode;
+    uiLocale: string | undefined;
     contentLanguageCode: LanguageCode;
     orderListLastCustomFilters: any;
     dashboardWidgetLayout: WidgetLayoutDefinition;

+ 1 - 0
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -205,6 +205,7 @@ export * from './shared/pipes/locale-currency-name.pipe';
 export * from './shared/pipes/locale-currency.pipe';
 export * from './shared/pipes/locale-date.pipe';
 export * from './shared/pipes/locale-language-name.pipe';
+export * from './shared/pipes/locale-region-name.pipe';
 export * from './shared/pipes/sentence-case.pipe';
 export * from './shared/pipes/sort.pipe';
 export * from './shared/pipes/state-i18n-token.pipe';

+ 6 - 3
packages/admin-ui/src/lib/core/src/shared/pipes/locale-base.pipe.ts

@@ -16,9 +16,12 @@ export abstract class LocaleBasePipe implements OnDestroy, PipeTransform {
         if (dataService && changeDetectorRef) {
             this.subscription = dataService.client
                 .uiState()
-                .mapStream(data => data.uiState.language)
-                .subscribe(languageCode => {
-                    this.locale = languageCode.replace(/_/g, '-');
+                .mapStream(data => data.uiState)
+                .subscribe(({ language, locale }) => {
+                    this.locale = language.replace(/_/g, '-');
+                    if (locale) {
+                        this.locale += `-${locale}`;
+                    }
                     changeDetectorRef.markForCheck();
                 });
         }

+ 6 - 0
packages/admin-ui/src/lib/core/src/shared/pipes/locale-language-name.pipe.spec.ts

@@ -13,6 +13,12 @@ describe('LocaleLanguageNamePipe', () => {
         expect(pipe.transform('zh_Hant', 'en')).toBe('Traditional Chinese');
     });
 
+    it('return correct names for language plus region', () => {
+        expect(pipe.transform('en-MY', 'en')).toBe('English (Malaysia)');
+        expect(pipe.transform('de-AT', 'de')).toBe('Österreichisches Deutsch');
+        expect(pipe.transform('zh_Hant-CN', 'en')).toBe('Traditional Chinese (China)');
+    });
+
     it('returns code for unknown codes', () => {
         expect(pipe.transform('xx')).toBe('xx');
     });

+ 30 - 0
packages/admin-ui/src/lib/core/src/shared/pipes/locale-region-name.pipe.spec.ts

@@ -0,0 +1,30 @@
+import { LocaleRegionNamePipe } from './locale-region-name.pipe';
+
+describe('LocaleRegionNamePipe', () => {
+    const pipe = new LocaleRegionNamePipe();
+    it('returns correct region names for various locales', () => {
+        expect(pipe.transform('GB', 'en')).toBe('United Kingdom');
+        expect(pipe.transform('AT', 'en')).toBe('Austria');
+        expect(pipe.transform('AT', 'de')).toBe('Österreich');
+        expect(pipe.transform('CN', 'zh')).toBe('中国');
+    });
+
+    it('returns region for unknown codes', () => {
+        expect(pipe.transform('xx')).toBe('xx');
+    });
+
+    it('returns empty string for empty input', () => {
+        expect(pipe.transform('')).toBe('');
+        expect(pipe.transform(null)).toBe('');
+        expect(pipe.transform(undefined)).toBe('');
+    });
+
+    it('returns warning for invalid input', () => {
+        expect(pipe.transform({} as any)).toBe('Invalid region code "[object Object]"');
+        expect(pipe.transform(false as any)).toBe('Invalid region code "false"');
+    });
+
+    it('returns input value for invalid string input', () => {
+        expect(pipe.transform('foo.bar')).toBe('foo.bar');
+    });
+});

+ 46 - 0
packages/admin-ui/src/lib/core/src/shared/pipes/locale-region-name.pipe.ts

@@ -0,0 +1,46 @@
+import { ChangeDetectorRef, Optional, Pipe, PipeTransform } from '@angular/core';
+
+import { DataService } from '../../data/providers/data.service';
+
+import { LocaleBasePipe } from './locale-base.pipe';
+
+/**
+ * @description
+ * Displays a human-readable name for a given region.
+ *
+ * @example
+ * ```HTML
+ * {{ 'GB' | localeRegionName }}
+ * ```
+ *
+ * @docsCategory pipes
+ */
+@Pipe({
+    name: 'localeRegionName',
+    pure: false,
+})
+export class LocaleRegionNamePipe extends LocaleBasePipe implements PipeTransform {
+    constructor(@Optional() dataService?: DataService, @Optional() changeDetectorRef?: ChangeDetectorRef) {
+        super(dataService, changeDetectorRef);
+    }
+    transform(value: any, locale?: unknown): string {
+        if (value == null || value === '') {
+            return '';
+        }
+        if (typeof value !== 'string') {
+            return `Invalid region code "${value as any}"`;
+        }
+        const activeLocale = typeof locale === 'string' ? locale : this.locale ?? 'en';
+
+        // Awaiting TS types for this API: https://github.com/microsoft/TypeScript/pull/44022/files
+        const DisplayNames = (Intl as any).DisplayNames;
+
+        try {
+            return new DisplayNames([activeLocale.replace('_', '-')], { type: 'region' }).of(
+                value.replace('_', '-'),
+            );
+        } catch (e) {
+            return value;
+        }
+    }
+}

+ 2 - 0
packages/admin-ui/src/lib/core/src/shared/shared.module.ts

@@ -120,6 +120,7 @@ import { LocaleCurrencyNamePipe } from './pipes/locale-currency-name.pipe';
 import { LocaleCurrencyPipe } from './pipes/locale-currency.pipe';
 import { LocaleDatePipe } from './pipes/locale-date.pipe';
 import { LocaleLanguageNamePipe } from './pipes/locale-language-name.pipe';
+import { LocaleRegionNamePipe } from './pipes/locale-region-name.pipe';
 import { SentenceCasePipe } from './pipes/sentence-case.pipe';
 import { SortPipe } from './pipes/sort.pipe';
 import { StateI18nTokenPipe } from './pipes/state-i18n-token.pipe';
@@ -221,6 +222,7 @@ const DECLARATIONS = [
     LocaleDatePipe,
     LocaleCurrencyPipe,
     LocaleLanguageNamePipe,
+    LocaleRegionNamePipe,
     TagSelectorComponent,
     ManageTagsDialogComponent,
     RelationSelectorDialogComponent,

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -170,6 +170,7 @@
     "add-new-variants": "Přidat {count, plural, one {variantu} few {{count} varianty} other {{count} variant}}",
     "add-note": "Přidat poznámku",
     "available-languages": "Dostupné jazyky",
+    "browser-default": "",
     "cancel": "Zrušit",
     "cancel-navigation": "Zrušit navigaci",
     "change-selection": "",
@@ -205,10 +206,12 @@
     "language": "Jazyk",
     "launch-extension": "Spustit rozšíření",
     "live-update": "Živé aktualizace",
+    "locale": "",
     "log-out": "Odhlásit",
     "login": "Přihlásit",
     "manage-tags": "",
     "manage-tags-description": "",
+    "medium-date": "",
     "more": "Více...",
     "name": "jméno",
     "no-results": "Žádné výsledky",
@@ -232,9 +235,12 @@
     "remove": "Smazat",
     "remove-item-from-list": "Odebrat položku ze seznamu",
     "results-count": "{ count } {count, plural, one {výsledek} other {výsledků/y}}",
+    "sample-formatting": "",
     "select": "Vybrat...",
     "select-display-language": "Vyberte jazyk",
     "select-today": "Vybrat dnešní datum",
+    "set-language": "",
+    "short-date": "",
     "tags": "",
     "theme": "Motiv",
     "there-are-unsaved-changes": "Provedené změny nebyly uloženy. Přechod na jinou stránku způsobí ztrátu těchto změn.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -170,6 +170,7 @@
     "add-new-variants": "{count, plural, one {1 Variante} other {{count} Varianten}} hinzufügen",
     "add-note": "Notiz hinzufügen",
     "available-languages": "Verfügbare Sprachen",
+    "browser-default": "",
     "cancel": "Abbrechen",
     "cancel-navigation": "Navigation abbrechen",
     "change-selection": "Auswahl ändern",
@@ -205,10 +206,12 @@
     "language": "Sprache",
     "launch-extension": "Erweiterung starten",
     "live-update": "Live-Aktualisierung",
+    "locale": "",
     "log-out": "Abmelden",
     "login": "Anmelden",
     "manage-tags": "Tags verwalten",
     "manage-tags-description": "Tagbeschreibungen verwalten",
+    "medium-date": "",
     "more": "Mehr...",
     "name": "Name",
     "no-results": "Keine Ergebnisse",
@@ -232,9 +235,12 @@
     "remove": "Entfernen",
     "remove-item-from-list": "Artikel von Liste entfernen",
     "results-count": "{ count } {count, plural, one {Ergebnis} other {Ergebnisse}}",
+    "sample-formatting": "",
     "select": "Auswählen...",
     "select-display-language": "Anzeigesprache wählen",
     "select-today": "Heute auswählen",
+    "set-language": "",
+    "short-date": "",
     "tags": "Tags",
     "theme": "Theme",
     "there-are-unsaved-changes": "Es gibt ungespeicherte Änderungen. Wenn Sie wechseln, gehen diese Änderungen verloren.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -170,6 +170,7 @@
     "add-new-variants": "Add {count, plural, one {1 variant} other {{count} variants}}",
     "add-note": "Add note",
     "available-languages": "Available languages",
+    "browser-default": "Browser default",
     "cancel": "Cancel",
     "cancel-navigation": "Cancel navigation",
     "change-selection": "Change selection",
@@ -205,10 +206,12 @@
     "language": "Language",
     "launch-extension": "Launch extension",
     "live-update": "Live update",
+    "locale": "Locale",
     "log-out": "Log out",
     "login": "Log in",
     "manage-tags": "Manage tags",
     "manage-tags-description": "Update or delete tags globally.",
+    "medium-date": "Medium date",
     "more": "More...",
     "name": "Name",
     "no-results": "No results",
@@ -232,9 +235,12 @@
     "remove": "Remove",
     "remove-item-from-list": "Remove item from list",
     "results-count": "{ count } {count, plural, one {result} other {results}}",
+    "sample-formatting": "Sample formatting",
     "select": "Select...",
     "select-display-language": "Select display language",
     "select-today": "Select today",
+    "set-language": "Set language",
+    "short-date": "Short date",
     "tags": "Tags",
     "theme": "Theme",
     "there-are-unsaved-changes": "There are unsaved changes. Navigating away will cause these changes to be lost.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -170,6 +170,7 @@
     "add-new-variants": "Añadir {count, plural, one {1 variante} other {{count} variantes}}",
     "add-note": "Añadir nota",
     "available-languages": "Idiomas disponibles",
+    "browser-default": "",
     "cancel": "Cancelar",
     "cancel-navigation": "Cancelar navegación",
     "change-selection": "Cambiar selección",
@@ -205,10 +206,12 @@
     "language": "Idioma",
     "launch-extension": "Ejecutar extensión",
     "live-update": "Actualización en vivo",
+    "locale": "",
     "log-out": "Salir",
     "login": "Entrar",
     "manage-tags": "Gestionar facetas",
     "manage-tags-description": "Actualiza o elimina facetas globalmente.",
+    "medium-date": "",
     "more": "Más...",
     "name": "Nombre",
     "no-results": "Sin resultados",
@@ -232,9 +235,12 @@
     "remove": "Borrar",
     "remove-item-from-list": "Eliminar elemento de la lista",
     "results-count": "{ count } {count, plural, one {resultado} other {resultados}}",
+    "sample-formatting": "",
     "select": "Seleccionar...",
     "select-display-language": "Seleccionar idioma de interfaz",
     "select-today": "Hoy",
+    "set-language": "",
+    "short-date": "",
     "tags": "Etiquetas",
     "theme": "Tema",
     "there-are-unsaved-changes": "Hay cambios sin guardar. Si sale de este sitio sus cambios se perderán.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -170,6 +170,7 @@
     "add-new-variants": "Ajout {count, plural, one {d'une variation} other {de {count} variations}}",
     "add-note": "Ajouter une note",
     "available-languages": "Langues disponibles",
+    "browser-default": "",
     "cancel": "Annuler",
     "cancel-navigation": "Annuler la navigation",
     "change-selection": "Modifier la sélection",
@@ -205,10 +206,12 @@
     "language": "Langue",
     "launch-extension": "Lancer extension",
     "live-update": "Mise à jour automatique",
+    "locale": "",
     "log-out": "Déconnexion",
     "login": "Connexion",
     "manage-tags": "Gérer les mot-clés",
     "manage-tags-description": "Mettre à jour ou supprimer les mots-clés de façon globale",
+    "medium-date": "",
     "more": "Plus...",
     "name": "Nom",
     "no-results": "Aucun resultat",
@@ -232,9 +235,12 @@
     "remove": "Retirer",
     "remove-item-from-list": "Retirer l'article de la liste",
     "results-count": "{ count } {count, plural, one {resultat} other {resultats}}",
+    "sample-formatting": "",
     "select": "Selectionner...",
     "select-display-language": "Choisir la langue d'affichage",
     "select-today": "Choisir aujourd'hui",
+    "set-language": "",
+    "short-date": "",
     "tags": "Mots-clés",
     "theme": "Thème",
     "there-are-unsaved-changes": "Il y a des changements non enregistrés. Naviguer ailleurs fera perdre ces changements.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/it.json

@@ -170,6 +170,7 @@
     "add-new-variants": "Aggiungi {count, plural, one {1 variante} other {{count} varianti}}",
     "add-note": "Aggiungi nota",
     "available-languages": "Lingue disponibili",
+    "browser-default": "",
     "cancel": "Annulla",
     "cancel-navigation": "Annulla navigazione",
     "change-selection": "Cambia selezione",
@@ -205,10 +206,12 @@
     "language": "Lingua",
     "launch-extension": "Lancia estensione",
     "live-update": "Aggiornamenti live",
+    "locale": "",
     "log-out": "Log out",
     "login": "Log in",
     "manage-tags": "Gestisci tag",
     "manage-tags-description": "Aggiungi o elimina tag globalmente",
+    "medium-date": "",
     "more": "Altri...",
     "name": "Nome",
     "no-results": "Nessun risultato",
@@ -232,9 +235,12 @@
     "remove": "Rimuovi",
     "remove-item-from-list": "Rimuovi elemento dalla lista",
     "results-count": "{ count } {count, plural, one {risultato} other {risultati}}",
+    "sample-formatting": "",
     "select": "Seleziona...",
     "select-display-language": "Seleziona lingua",
     "select-today": "Seleziona oggi",
+    "set-language": "",
+    "short-date": "",
     "tags": "Tag",
     "theme": "Tema",
     "there-are-unsaved-changes": "Ci sono modifiche non salvate. Lasciando questa pagina le modifiche andranno perse.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -170,6 +170,7 @@
     "add-new-variants": "Dodaj {count, plural, one {1 wariant} other {{count} wariantów}}",
     "add-note": "",
     "available-languages": "Dostępne języki",
+    "browser-default": "",
     "cancel": "Anuluj",
     "cancel-navigation": "Anuluj nawigacje",
     "change-selection": "",
@@ -205,10 +206,12 @@
     "language": "Język",
     "launch-extension": "Uruchom rozszerzenie",
     "live-update": "Aktualizacja live",
+    "locale": "",
     "log-out": "Wyloguj",
     "login": "Zaloguj",
     "manage-tags": "",
     "manage-tags-description": "",
+    "medium-date": "",
     "more": "Więcej...",
     "name": "Nazwa",
     "no-results": "Brak wyników",
@@ -232,9 +235,12 @@
     "remove": "Usuń",
     "remove-item-from-list": "",
     "results-count": "{ count } {count, plural, one {wynik} other {wyników}}",
+    "sample-formatting": "",
     "select": "Wybrano...",
     "select-display-language": "Wybierz język",
     "select-today": "Wybierz dzisiaj",
+    "set-language": "",
+    "short-date": "",
     "tags": "",
     "theme": "",
     "there-are-unsaved-changes": "Są nie zapisane zmiany. Nawigacja do innej lokalizacji spowoduje utrate zmian.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -170,6 +170,7 @@
     "add-new-variants": "Adicionar {count, plural, one {1 variant} other {{count} variants}}",
     "add-note": "Adicionar nota",
     "available-languages": "Idiomas disponíveis",
+    "browser-default": "",
     "cancel": "Cancelar",
     "cancel-navigation": "Cancelar navegação",
     "change-selection": "",
@@ -205,10 +206,12 @@
     "language": "Idioma",
     "launch-extension": "Iniciar extensão",
     "live-update": "Atualização ao vivo",
+    "locale": "",
     "log-out": "Sair",
     "login": "Entrar",
     "manage-tags": "",
     "manage-tags-description": "",
+    "medium-date": "",
     "more": "Mais...",
     "name": "Nome",
     "no-results": "Sem resultados",
@@ -232,9 +235,12 @@
     "remove": "Exclui",
     "remove-item-from-list": "",
     "results-count": "{ count } {count, plural, one {result} other {results}}",
+    "sample-formatting": "",
     "select": "Selecione...",
     "select-display-language": "Selecionar idioma de exibição",
     "select-today": "Selecione hoje",
+    "set-language": "",
+    "short-date": "",
     "tags": "",
     "theme": "Tema",
     "there-are-unsaved-changes": "Há alterações não salvas. Navegar para outra página fará com que essas alterações sejam perdidas.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -170,6 +170,7 @@
     "add-new-variants": "Adicionar {count, plural, one {variante} other {{count} variantes}}",
     "add-note": "Adicionar nota",
     "available-languages": "Idiomas disponíveis",
+    "browser-default": "",
     "cancel": "Cancelar",
     "cancel-navigation": "Continuar a editar",
     "change-selection": "Alterar seleccionados",
@@ -205,10 +206,12 @@
     "language": "Idioma",
     "launch-extension": "Iniciar extensão",
     "live-update": "Actualização em tempo real",
+    "locale": "",
     "log-out": "Sair",
     "login": "Entrar",
     "manage-tags": "Gerir tags",
     "manage-tags-description": "Atualize ou elimine tags globalmente.",
+    "medium-date": "",
     "more": "Mais...",
     "name": "Nome",
     "no-results": "Nenhum resultado encontrado",
@@ -232,9 +235,12 @@
     "remove": "Eliminar",
     "remove-item-from-list": "Remover item da lista",
     "results-count": "{ count } {count, plural, one {resultado} other {resultados}}",
+    "sample-formatting": "",
     "select": "Seleccione...",
     "select-display-language": "Seleccionar idioma",
     "select-today": "Seleccione a data de hoje",
+    "set-language": "",
+    "short-date": "",
     "tags": "Tags",
     "theme": "Tema",
     "there-are-unsaved-changes": "Há alterações por guardar. Navegar para outra página fará com que essas alterações sejam perdidas.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -170,6 +170,7 @@
     "add-new-variants": "Добавить {count, plural, one {1 вариант} other {{count} вариантов}}",
     "add-note": "Добавить заметку",
     "available-languages": "Доступные языки",
+    "browser-default": "",
     "cancel": "Отмена",
     "cancel-navigation": "Отменить навигацию",
     "change-selection": "Изменить выбор",
@@ -205,10 +206,12 @@
     "language": "Язык",
     "launch-extension": "Запуск расширения",
     "live-update": "Обновление в режиме реального времени",
+    "locale": "",
     "log-out": "Выйти",
     "login": "Войти",
     "manage-tags": "Управление тегами",
     "manage-tags-description": "Обновление или удаление тегов глобально.",
+    "medium-date": "",
     "more": "Больше...",
     "name": "Имя",
     "no-results": "Нет результатов",
@@ -232,9 +235,12 @@
     "remove": "Удалить",
     "remove-item-from-list": "Удалить позицию из списка",
     "results-count": "{ count } {count, plural, one {результат} other {результатов}}",
+    "sample-formatting": "",
     "select": "Выбрать...",
     "select-display-language": "Выберите язык отображения",
     "select-today": "Выберите сегодня",
+    "set-language": "",
+    "short-date": "",
     "tags": "Теги",
     "theme": "Тема",
     "there-are-unsaved-changes": "Есть несохраненные изменения. Если вы выйдете, эти изменения будут потеряны.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/uk.json

@@ -170,6 +170,7 @@
     "add-new-variants": "Додати {count, plural, one {1 варіант} other {{count} варіантів}}",
     "add-note": "Додати замітку",
     "available-languages": "Доступні мови",
+    "browser-default": "",
     "cancel": "Скасування",
     "cancel-navigation": "Скасувати навігацію",
     "change-selection": "Змінити вибір",
@@ -205,10 +206,12 @@
     "language": "Мова",
     "launch-extension": "Запуск розширення",
     "live-update": "Оновлення в режимі реального часу",
+    "locale": "",
     "log-out": "Вийти",
     "login": "Увійти",
     "manage-tags": "Керування тегами",
     "manage-tags-description": "Оновлення або видалення тегів глобально.",
+    "medium-date": "",
     "more": "Більше...",
     "name": "Ім'я",
     "no-results": "Немає результатів",
@@ -232,9 +235,12 @@
     "remove": "Видалити",
     "remove-item-from-list": "Видалити позицію зі списку",
     "results-count": "{ count } {count, plural, one {результат} other {результатів}}",
+    "sample-formatting": "",
     "select": "Вибрати...",
     "select-display-language": "Виберіть мову відображення",
     "select-today": "Виберіть сьогодні",
+    "set-language": "",
+    "short-date": "",
     "tags": "Теги",
     "theme": "Тема",
     "there-are-unsaved-changes": "Є незбережені зміни. Якщо ви вийдете, ці зміни будуть втрачені.",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -170,6 +170,7 @@
     "add-new-variants": "添加{count}个商品规格",
     "add-note": "添加注释",
     "available-languages": "可用语言",
+    "browser-default": "",
     "cancel": "取消",
     "cancel-navigation": "取消",
     "change-selection": "更改选项",
@@ -205,10 +206,12 @@
     "language": "语言",
     "launch-extension": "启动扩展插件",
     "live-update": "在线更新",
+    "locale": "",
     "log-out": "退出",
     "login": "登陆",
     "manage-tags": "管理标签",
     "manage-tags-description": "更新或删除标签",
+    "medium-date": "",
     "more": "更多...",
     "name": "名称",
     "no-results": "没找到任何结果",
@@ -232,9 +235,12 @@
     "remove": "删除",
     "remove-item-from-list": "从列表中移除",
     "results-count": "{count, plural, 0{无} other {{count}个过滤结果}}",
+    "sample-formatting": "",
     "select": "选择...",
     "select-display-language": "选择显示语言",
     "select-today": "选择今天",
+    "set-language": "",
+    "short-date": "",
     "tags": "标签",
     "theme": "主题",
     "there-are-unsaved-changes": "修改尚未被保存,现在离开会导致您的修改会被删除",

+ 6 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -170,6 +170,7 @@
     "add-new-variants": "新增{count}個商品規格",
     "add-note": "",
     "available-languages": "可用語言",
+    "browser-default": "",
     "cancel": "取消",
     "cancel-navigation": "取消",
     "change-selection": "",
@@ -205,10 +206,12 @@
     "language": "語言",
     "launch-extension": "启動扩展插件",
     "live-update": "",
+    "locale": "",
     "log-out": "退出",
     "login": "登陆",
     "manage-tags": "",
     "manage-tags-description": "",
+    "medium-date": "",
     "more": "更多...",
     "name": "名稱",
     "no-results": "没找到任何結果",
@@ -232,9 +235,12 @@
     "remove": "移除",
     "remove-item-from-list": "",
     "results-count": "{count, plural, 0{無} other {{count}個篩選結果}}",
+    "sample-formatting": "",
     "select": "選擇...",
     "select-display-language": "選擇顯示語言",
     "select-today": "選擇今天",
+    "set-language": "",
+    "short-date": "",
     "tags": "",
     "theme": "",
     "there-are-unsaved-changes": "變更尚未被儲存,離開會失去所有變更",

+ 4 - 1
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -2291,7 +2291,10 @@ export type Mutation = {
      */
     modifyOrder: ModifyOrderResult;
     /**
-     * Used to manually create a new Payment against an Order. This is used when a completed Order
+     * Used to manually create a new Payment against an Order.
+     * This can be used by an Administrator when an Order is in the ArrangingPayment state.
+     *
+     * It is also used when a completed Order
      * has been modified (using `modifyOrder`) and the price has increased. The extra payment
      * can then be manually arranged by the administrator, and the details used to create a new
      * Payment.

+ 1 - 1
packages/common/src/generated-shop-types.ts

@@ -2129,7 +2129,7 @@ export type PaymentFailedError = ErrorResult & {
 
 /** Passed as input to the `addPaymentToOrder` mutation. */
 export type PaymentInput = {
-    /** This field should correspond to the `code` property of a PaymentMethodHandler. */
+    /** This field should correspond to the `code` property of a PaymentMethod. */
     method: Scalars['String'];
     /**
      * This field should contain arbitrary data passed to the specified PaymentMethodHandler's `createPayment()` method

+ 4 - 1
packages/common/src/generated-types.ts

@@ -2337,7 +2337,10 @@ export type Mutation = {
    */
   modifyOrder: ModifyOrderResult;
   /**
-   * Used to manually create a new Payment against an Order. This is used when a completed Order
+   * Used to manually create a new Payment against an Order.
+   * This can be used by an Administrator when an Order is in the ArrangingPayment state.
+   *
+   * It is also used when a completed Order
    * has been modified (using `modifyOrder`) and the price has increased. The extra payment
    * can then be manually arranged by the administrator, and the details used to create a new
    * Payment.

+ 8 - 0
packages/common/src/shared-types.ts

@@ -256,6 +256,14 @@ export interface AdminUiConfig {
      * @default LanguageCode.en
      */
     defaultLanguage: LanguageCode;
+    /**
+     * @description
+     * The default locale for the Admin UI. The locale affects the formatting of
+     * currencies & dates.
+     *
+     * If not set, the browser default locale will be used.
+     */
+    defaultLocale?: string;
     /**
      * @description
      * An array of languages for which translations exist for the Admin UI.

+ 4 - 1
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -2291,7 +2291,10 @@ export type Mutation = {
      */
     modifyOrder: ModifyOrderResult;
     /**
-     * Used to manually create a new Payment against an Order. This is used when a completed Order
+     * Used to manually create a new Payment against an Order.
+     * This can be used by an Administrator when an Order is in the ArrangingPayment state.
+     *
+     * It is also used when a completed Order
      * has been modified (using `modifyOrder`) and the price has increased. The extra payment
      * can then be manually arranged by the administrator, and the details used to create a new
      * Payment.

+ 1 - 1
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -2055,7 +2055,7 @@ export type PaymentFailedError = ErrorResult & {
 
 /** Passed as input to the `addPaymentToOrder` mutation. */
 export type PaymentInput = {
-    /** This field should correspond to the `code` property of a PaymentMethodHandler. */
+    /** This field should correspond to the `code` property of a PaymentMethod. */
     method: Scalars['String'];
     /**
      * This field should contain arbitrary data passed to the specified PaymentMethodHandler's `createPayment()` method

+ 1 - 12
packages/dev-server/dev-config.ts

@@ -55,18 +55,7 @@ export const devConfig: VendureConfig = {
     paymentOptions: {
         paymentMethodHandlers: [dummyPaymentHandler],
     },
-    customFields: {
-        // Country: [
-        //     { name: 'foo', type: 'localeString' },
-        //     { name: 'rating', type: 'int' },
-        // ],
-        // CustomerGroup: [{ name: 'foo', type: 'string' }],
-        // PaymentMethod: [{ name: 'foo', type: 'string' }],
-        // Promotion: [{ name: 'foo', type: 'string' }],
-        // TaxCategory: [{ name: 'foo', type: 'string' }],
-        // TaxRate: [{ name: 'foo', type: 'string' }],
-        // Zone: [{ name: 'foo', type: 'string' }],
-    },
+    customFields: {},
     logger: new DefaultLogger({ level: LogLevel.Debug }),
     importExportOptions: {
         importAssetsDir: path.join(__dirname, 'import-assets'),

+ 4 - 1
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -2291,7 +2291,10 @@ export type Mutation = {
      */
     modifyOrder: ModifyOrderResult;
     /**
-     * Used to manually create a new Payment against an Order. This is used when a completed Order
+     * Used to manually create a new Payment against an Order.
+     * This can be used by an Administrator when an Order is in the ArrangingPayment state.
+     *
+     * It is also used when a completed Order
      * has been modified (using `modifyOrder`) and the price has increased. The extra payment
      * can then be manually arranged by the administrator, and the details used to create a new
      * Payment.

+ 4 - 1
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -2291,7 +2291,10 @@ export type Mutation = {
      */
     modifyOrder: ModifyOrderResult;
     /**
-     * Used to manually create a new Payment against an Order. This is used when a completed Order
+     * Used to manually create a new Payment against an Order.
+     * This can be used by an Administrator when an Order is in the ArrangingPayment state.
+     *
+     * It is also used when a completed Order
      * has been modified (using `modifyOrder`) and the price has increased. The extra payment
      * can then be manually arranged by the administrator, and the details used to create a new
      * Payment.

+ 1 - 1
packages/payments-plugin/e2e/graphql/generated-shop-types.ts

@@ -2055,7 +2055,7 @@ export type PaymentFailedError = ErrorResult & {
 
 /** Passed as input to the `addPaymentToOrder` mutation. */
 export type PaymentInput = {
-    /** This field should correspond to the `code` property of a PaymentMethodHandler. */
+    /** This field should correspond to the `code` property of a PaymentMethod. */
     method: Scalars['String'];
     /**
      * This field should contain arbitrary data passed to the specified PaymentMethodHandler's `createPayment()` method

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
schema-admin.json


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
schema-shop.json


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác