From 204c7360c3e345ad2084d827f6bf095fc3aeb27f Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Tue, 5 Apr 2022 21:02:07 +0200
Subject: [PATCH] Feature/prepare for localized date format (#803)

* Support localized date and number format

* Update changelog
---
 CHANGELOG.md                                  |  6 +++
 .../interfaces/user-settings.interface.ts     |  1 +
 .../src/app/user/update-user-setting.dto.ts   |  6 ++-
 apps/api/src/app/user/user.controller.ts      | 18 ++++----
 apps/api/src/app/user/user.service.ts         | 21 +++++----
 apps/client/src/app/adapter/date-formats.ts   | 12 +++--
 .../admin-market-data-detail.component.ts     | 12 +++--
 .../admin-market-data.component.ts            | 25 ++++++++---
 .../admin-market-data/admin-market-data.html  |  1 +
 .../admin-overview.component.ts               |  2 -
 .../portfolio-performance.component.ts        |  7 ++-
 .../pages/account/account-page.component.ts   | 32 ++++++++++++-
 .../src/app/pages/account/account-page.html   | 25 +++++++++++
 .../app/pages/account/account-page.module.ts  |  2 +
 apps/client/src/main.ts                       |  3 +-
 libs/common/src/lib/config.ts                 |  3 +-
 libs/common/src/lib/helper.ts                 | 45 ++++++++++++++++++-
 .../activities-table.component.ts             |  6 ++-
 libs/ui/src/lib/value/value.component.ts      | 17 ++++---
 19 files changed, 193 insertions(+), 51 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e584bb04..1c918a903 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## Unreleased
+
+### Added
+
+- Added support for localization (date and number format) in user settings
+
 ## 1.131.1 - 04.04.2022
 
 ### Fixed
diff --git a/apps/api/src/app/user/interfaces/user-settings.interface.ts b/apps/api/src/app/user/interfaces/user-settings.interface.ts
index fb4b2af35..ef3b03f1b 100644
--- a/apps/api/src/app/user/interfaces/user-settings.interface.ts
+++ b/apps/api/src/app/user/interfaces/user-settings.interface.ts
@@ -1,5 +1,6 @@
 export interface UserSettings {
   emergencyFund?: number;
+  locale?: string;
   isNewCalculationEngine?: boolean;
   isRestrictedView?: boolean;
 }
diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts
index 5af2f5f8d..eaa41464a 100644
--- a/apps/api/src/app/user/update-user-setting.dto.ts
+++ b/apps/api/src/app/user/update-user-setting.dto.ts
@@ -1,4 +1,4 @@
-import { IsBoolean, IsNumber, IsOptional } from 'class-validator';
+import { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';
 
 export class UpdateUserSettingDto {
   @IsNumber()
@@ -12,4 +12,8 @@ export class UpdateUserSettingDto {
   @IsBoolean()
   @IsOptional()
   isRestrictedView?: boolean;
+
+  @IsString()
+  @IsOptional()
+  locale?: string;
 }
diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts
index 97cf25b6e..5bd14cfaa 100644
--- a/apps/api/src/app/user/user.controller.ts
+++ b/apps/api/src/app/user/user.controller.ts
@@ -2,17 +2,14 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
 import { PropertyService } from '@ghostfolio/api/services/property/property.service';
 import { PROPERTY_IS_READ_ONLY_MODE } from '@ghostfolio/common/config';
 import { User } from '@ghostfolio/common/interfaces';
-import {
-  hasPermission,
-  hasRole,
-  permissions
-} from '@ghostfolio/common/permissions';
+import { hasPermission, permissions } from '@ghostfolio/common/permissions';
 import type { RequestWithUser } from '@ghostfolio/common/types';
 import {
   Body,
   Controller,
   Delete,
   Get,
+  Headers,
   HttpException,
   Inject,
   Param,
@@ -63,8 +60,13 @@ export class UserController {
 
   @Get()
   @UseGuards(AuthGuard('jwt'))
-  public async getUser(@Param('id') id: string): Promise<User> {
-    return this.userService.getUser(this.request.user);
+  public async getUser(
+    @Headers('accept-language') acceptLanguage: string
+  ): Promise<User> {
+    return this.userService.getUser(
+      this.request.user,
+      acceptLanguage?.split(',')?.[0]
+    );
   }
 
   @Post()
@@ -118,7 +120,7 @@ export class UserController {
     };
 
     for (const key in userSettings) {
-      if (userSettings[key] === false) {
+      if (userSettings[key] === false || userSettings[key] === null) {
         delete userSettings[key];
       }
     }
diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts
index 43b3b52c7..a4e9267ef 100644
--- a/apps/api/src/app/user/user.service.ts
+++ b/apps/api/src/app/user/user.service.ts
@@ -33,14 +33,17 @@ export class UserService {
     private readonly subscriptionService: SubscriptionService
   ) {}
 
-  public async getUser({
-    Account,
-    alias,
-    id,
-    permissions,
-    Settings,
-    subscription
-  }: UserWithSettings): Promise<IUser> {
+  public async getUser(
+    {
+      Account,
+      alias,
+      id,
+      permissions,
+      Settings,
+      subscription
+    }: UserWithSettings,
+    aLocale = locale
+  ): Promise<IUser> {
     const access = await this.prismaService.access.findMany({
       include: {
         User: true
@@ -63,8 +66,8 @@ export class UserService {
       accounts: Account,
       settings: {
         ...(<UserSettings>Settings.settings),
-        locale,
         baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
+        locale: (<UserSettings>Settings.settings).locale ?? aLocale,
         viewMode: Settings?.viewMode ?? ViewMode.DEFAULT
       }
     };
diff --git a/apps/client/src/app/adapter/date-formats.ts b/apps/client/src/app/adapter/date-formats.ts
index 554f7c76e..fdf32bef8 100644
--- a/apps/client/src/app/adapter/date-formats.ts
+++ b/apps/client/src/app/adapter/date-formats.ts
@@ -1,16 +1,14 @@
-import {
-  DEFAULT_DATE_FORMAT,
-  DEFAULT_DATE_FORMAT_MONTH_YEAR
-} from '@ghostfolio/common/config';
+import { DEFAULT_DATE_FORMAT_MONTH_YEAR } from '@ghostfolio/common/config';
+import { getDateFormatString } from '@ghostfolio/common/helper';
 
 export const DateFormats = {
   display: {
-    dateInput: DEFAULT_DATE_FORMAT,
+    dateInput: getDateFormatString(),
     monthYearLabel: DEFAULT_DATE_FORMAT_MONTH_YEAR,
-    dateA11yLabel: DEFAULT_DATE_FORMAT,
+    dateA11yLabel: getDateFormatString(),
     monthYearA11yLabel: DEFAULT_DATE_FORMAT_MONTH_YEAR
   },
   parse: {
-    dateInput: DEFAULT_DATE_FORMAT
+    dateInput: getDateFormatString()
   }
 };
diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts
index 9f935cb91..f28253b90 100644
--- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts
+++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts
@@ -8,8 +8,11 @@ import {
   Output
 } from '@angular/core';
 import { MatDialog } from '@angular/material/dialog';
-import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
-import { DATE_FORMAT } from '@ghostfolio/common/helper';
+import {
+  DATE_FORMAT,
+  getDateFormatString,
+  getLocale
+} from '@ghostfolio/common/helper';
 import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
 import { DataSource, MarketData } from '@prisma/client';
 import {
@@ -35,13 +38,14 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog/market-data-
 export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
   @Input() dataSource: DataSource;
   @Input() dateOfFirstActivity: string;
+  @Input() locale = getLocale();
   @Input() marketData: MarketData[];
   @Input() symbol: string;
 
   @Output() marketDataChanged = new EventEmitter<boolean>();
 
   public days = Array(31);
-  public defaultDateFormat = DEFAULT_DATE_FORMAT;
+  public defaultDateFormat: string;
   public deviceType: string;
   public historicalDataItems: LineChartItem[];
   public marketDataByMonth: {
@@ -62,6 +66,8 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
   public ngOnInit() {}
 
   public ngOnChanges() {
+    this.defaultDateFormat = getDateFormatString(this.locale);
+
     this.historicalDataItems = this.marketData.map((marketDataItem) => {
       return {
         date: format(marketDataItem.date, DATE_FORMAT),
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
index a2900ae6b..2229a3609 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
@@ -7,8 +7,9 @@ import {
 } from '@angular/core';
 import { AdminService } from '@ghostfolio/client/services/admin.service';
 import { DataService } from '@ghostfolio/client/services/data.service';
-import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
-import { UniqueAsset } from '@ghostfolio/common/interfaces';
+import { UserService } from '@ghostfolio/client/services/user/user.service';
+import { getDateFormatString } from '@ghostfolio/common/helper';
+import { UniqueAsset, User } from '@ghostfolio/common/interfaces';
 import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
 import { DataSource, MarketData } from '@prisma/client';
 import { Subject } from 'rxjs';
@@ -23,9 +24,10 @@ import { takeUntil } from 'rxjs/operators';
 export class AdminMarketDataComponent implements OnDestroy, OnInit {
   public currentDataSource: DataSource;
   public currentSymbol: string;
-  public defaultDateFormat = DEFAULT_DATE_FORMAT;
+  public defaultDateFormat: string;
   public marketData: AdminMarketDataItem[] = [];
   public marketDataDetails: MarketData[] = [];
+  public user: User;
 
   private unsubscribeSubject = new Subject<void>();
 
@@ -35,8 +37,21 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
   public constructor(
     private adminService: AdminService,
     private changeDetectorRef: ChangeDetectorRef,
-    private dataService: DataService
-  ) {}
+    private dataService: DataService,
+    private userService: UserService
+  ) {
+    this.userService.stateChanged
+      .pipe(takeUntil(this.unsubscribeSubject))
+      .subscribe((state) => {
+        if (state?.user) {
+          this.user = state.user;
+
+          this.defaultDateFormat = getDateFormatString(
+            this.user.settings.locale
+          );
+        }
+      });
+  }
 
   /**
    * Initializes the controller
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html
index 7638d6110..725c75e22 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.html
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html
@@ -65,6 +65,7 @@
                 <gf-admin-market-data-detail
                   [dataSource]="item.dataSource"
                   [dateOfFirstActivity]="item.date"
+                  [locale]="user?.settings?.locale"
                   [marketData]="marketDataDetails"
                   [symbol]="item.symbol"
                   (marketDataChanged)="onMarketDataChanged($event)"
diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts
index 013633c00..f115c4ca9 100644
--- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts
+++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts
@@ -5,7 +5,6 @@ import { CacheService } from '@ghostfolio/client/services/cache.service';
 import { DataService } from '@ghostfolio/client/services/data.service';
 import { UserService } from '@ghostfolio/client/services/user/user.service';
 import {
-  DEFAULT_DATE_FORMAT,
   PROPERTY_COUPONS,
   PROPERTY_CURRENCIES,
   PROPERTY_IS_READ_ONLY_MODE,
@@ -35,7 +34,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
   public customCurrencies: string[];
   public dataGatheringInProgress: boolean;
   public dataGatheringProgress: number;
-  public defaultDateFormat = DEFAULT_DATE_FORMAT;
   public exchangeRates: { label1: string; label2: string; value: number }[];
   public hasPermissionForSubscription: boolean;
   public hasPermissionForSystemMessage: boolean;
diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
index a2607b585..f1daa7e72 100644
--- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
+++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
@@ -7,6 +7,10 @@ import {
   OnInit,
   ViewChild
 } from '@angular/core';
+import {
+  getNumberFormatDecimal,
+  getNumberFormatGroup
+} from '@ghostfolio/common/helper';
 import {
   PortfolioPerformance,
   ResponseError
@@ -50,13 +54,14 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
         this.unit = this.baseCurrency;
 
         new CountUp('value', this.performance?.currentValue, {
+          decimal: getNumberFormatDecimal(this.locale),
           decimalPlaces:
             this.deviceType === 'mobile' &&
             this.performance?.currentValue >= 100000
               ? 0
               : 2,
           duration: 1,
-          separator: `'`
+          separator: getNumberFormatGroup(this.locale)
         }).start();
       } else if (this.performance?.currentValue === null) {
         this.unit = '%';
diff --git a/apps/client/src/app/pages/account/account-page.component.ts b/apps/client/src/app/pages/account/account-page.component.ts
index 8352dc35e..072d91482 100644
--- a/apps/client/src/app/pages/account/account-page.component.ts
+++ b/apps/client/src/app/pages/account/account-page.component.ts
@@ -20,9 +20,11 @@ import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
 import { DataService } from '@ghostfolio/client/services/data.service';
 import { UserService } from '@ghostfolio/client/services/user/user.service';
 import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
-import { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config';
+import { baseCurrency } from '@ghostfolio/common/config';
+import { getDateFormatString } from '@ghostfolio/common/helper';
 import { Access, User } from '@ghostfolio/common/interfaces';
 import { hasPermission, permissions } from '@ghostfolio/common/permissions';
+import { uniq } from 'lodash';
 import { DeviceDetectorService } from 'ngx-device-detector';
 import { StripeService } from 'ngx-stripe';
 import { EMPTY, Subject } from 'rxjs';
@@ -45,13 +47,14 @@ export class AccountPageComponent implements OnDestroy, OnInit {
   public coupon: number;
   public couponId: string;
   public currencies: string[] = [];
-  public defaultDateFormat = DEFAULT_DATE_FORMAT;
+  public defaultDateFormat: string;
   public deviceType: string;
   public hasPermissionForSubscription: boolean;
   public hasPermissionToCreateAccess: boolean;
   public hasPermissionToDeleteAccess: boolean;
   public hasPermissionToUpdateViewMode: boolean;
   public hasPermissionToUpdateUserSettings: boolean;
+  public locales = ['de', 'de-CH', 'en-GB', 'en-US'];
   public price: number;
   public priceId: string;
   public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
@@ -101,6 +104,10 @@ export class AccountPageComponent implements OnDestroy, OnInit {
         if (state?.user) {
           this.user = state.user;
 
+          this.defaultDateFormat = getDateFormatString(
+            this.user.settings.locale
+          );
+
           this.hasPermissionToCreateAccess = hasPermission(
             this.user.permissions,
             permissions.createAccess
@@ -121,6 +128,9 @@ export class AccountPageComponent implements OnDestroy, OnInit {
             permissions.updateViewMode
           );
 
+          this.locales.push(this.user.settings.locale);
+          this.locales = uniq(this.locales.sort());
+
           this.changeDetectorRef.markForCheck();
         }
       });
@@ -143,6 +153,24 @@ export class AccountPageComponent implements OnDestroy, OnInit {
     this.update();
   }
 
+  public onChangeUserSetting(aKey: string, aValue: string) {
+    this.dataService
+      .putUserSetting({ [aKey]: aValue })
+      .pipe(takeUntil(this.unsubscribeSubject))
+      .subscribe(() => {
+        this.userService.remove();
+
+        this.userService
+          .get()
+          .pipe(takeUntil(this.unsubscribeSubject))
+          .subscribe((user) => {
+            this.user = user;
+
+            this.changeDetectorRef.markForCheck();
+          });
+      });
+  }
+
   public onChangeUserSettings(aKey: string, aValue: string) {
     const settings = { ...this.user.settings, [aKey]: aValue };
 
diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html
index 47f8a3714..96cc04d16 100644
--- a/apps/client/src/app/pages/account/account-page.html
+++ b/apps/client/src/app/pages/account/account-page.html
@@ -111,6 +111,31 @@
                   </mat-form-field>
                 </div>
               </div>
+              <div class="align-items-center d-flex mb-2">
+                <div class="pr-1 w-50">
+                  <div i18n>Locale</div>
+                  <div class="hint-text text-muted" i18n>
+                    Date and number format
+                  </div>
+                </div>
+                <div class="pl-1 w-50">
+                  <mat-form-field appearance="outline" class="w-100">
+                    <mat-select
+                      name="locale"
+                      [disabled]="!hasPermissionToUpdateUserSettings"
+                      [value]="user.settings.locale"
+                      (selectionChange)="onChangeUserSetting('locale', $event.value)"
+                    >
+                      <mat-option [value]="null"></mat-option>
+                      <mat-option
+                        *ngFor="let locale of locales"
+                        [value]="locale"
+                        >{{ locale }}</mat-option
+                      >
+                    </mat-select>
+                  </mat-form-field>
+                </div>
+              </div>
               <div class="d-flex">
                 <div class="align-items-center d-flex pr-1 pt-1 w-50" i18n>
                   View Mode
diff --git a/apps/client/src/app/pages/account/account-page.module.ts b/apps/client/src/app/pages/account/account-page.module.ts
index cf0f52a03..d583e47cd 100644
--- a/apps/client/src/app/pages/account/account-page.module.ts
+++ b/apps/client/src/app/pages/account/account-page.module.ts
@@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
 import { MatSlideToggleModule } from '@angular/material/slide-toggle';
 import { RouterModule } from '@angular/router';
 import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
+import { GfValueModule } from '@ghostfolio/ui/value';
 
 import { AccountPageRoutingModule } from './account-page-routing.module';
 import { AccountPageComponent } from './account-page.component';
@@ -24,6 +25,7 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
     FormsModule,
     GfCreateOrUpdateAccessDialogModule,
     GfPortfolioAccessTableModule,
+    GfValueModule,
     MatButtonModule,
     MatCardModule,
     MatDialogModule,
diff --git a/apps/client/src/main.ts b/apps/client/src/main.ts
index 54e4b175a..915236117 100644
--- a/apps/client/src/main.ts
+++ b/apps/client/src/main.ts
@@ -1,6 +1,7 @@
 import { enableProdMode } from '@angular/core';
 import { LOCALE_ID } from '@angular/core';
 import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+import { locale } from '@ghostfolio/common/config';
 import { InfoItem } from '@ghostfolio/common/interfaces';
 import { permissions } from '@ghostfolio/common/permissions';
 
@@ -27,7 +28,7 @@ import { environment } from './environments/environment';
 
   platformBrowserDynamic()
     .bootstrapModule(AppModule, {
-      providers: [{ provide: LOCALE_ID, useValue: 'de-CH' }]
+      providers: [{ provide: LOCALE_ID, useValue: locale }]
     })
     .catch((error) => console.error(error));
 })();
diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts
index 535fa2ef3..133dbeef6 100644
--- a/libs/common/src/lib/config.ts
+++ b/libs/common/src/lib/config.ts
@@ -19,7 +19,7 @@ export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;
 export const ghostfolioFearAndGreedIndexDataSource = DataSource.RAKUTEN;
 export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`;
 
-export const locale = 'de-CH';
+export const locale = 'en-US';
 
 export const primaryColorHex = '#36cfcc';
 export const primaryColorRgb = {
@@ -44,7 +44,6 @@ export const warnColorRgb = {
 
 export const ASSET_SUB_CLASS_EMERGENCY_FUND = 'EMERGENCY_FUND';
 
-export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy';
 export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
 
 export const PROPERTY_COUPONS = 'COUPONS';
diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts
index e337493c7..2e45d40cd 100644
--- a/libs/common/src/lib/helper.ts
+++ b/libs/common/src/lib/helper.ts
@@ -2,7 +2,7 @@ import * as currencies from '@dinero.js/currencies';
 import { DataSource } from '@prisma/client';
 import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
 
-import { ghostfolioScraperApiSymbolPrefix } from './config';
+import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
 
 export function capitalize(aString: string) {
   return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
@@ -44,6 +44,49 @@ export function getCssVariable(aCssVariable: string) {
   );
 }
 
+export function getDateFormatString(aLocale?: string) {
+  const formatObject = new Intl.DateTimeFormat(aLocale).formatToParts(
+    new Date()
+  );
+
+  return formatObject
+    .map((object) => {
+      switch (object.type) {
+        case 'day':
+          return 'dd';
+        case 'month':
+          return 'MM';
+        case 'year':
+          return 'yyyy';
+        default:
+          return object.value;
+      }
+    })
+    .join('');
+}
+
+export function getLocale() {
+  return navigator.languages?.length
+    ? navigator.languages[0]
+    : navigator.language ?? locale;
+}
+
+export function getNumberFormatDecimal(aLocale?: string) {
+  const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99);
+
+  return formatObject.find((object) => {
+    return object.type === 'decimal';
+  }).value;
+}
+
+export function getNumberFormatGroup(aLocale?: string) {
+  const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99);
+
+  return formatObject.find((object) => {
+    return object.type === 'group';
+  }).value;
+}
+
 export function getTextColor() {
   const cssVariable = getCssVariable(
     window.matchMedia('(prefers-color-scheme: dark)').matches
diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts
index b8d8b0597..52bc841ff 100644
--- a/libs/ui/src/lib/activities-table/activities-table.component.ts
+++ b/libs/ui/src/lib/activities-table/activities-table.component.ts
@@ -20,7 +20,7 @@ import { MatSort } from '@angular/material/sort';
 import { MatTableDataSource } from '@angular/material/table';
 import { Router } from '@angular/router';
 import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
-import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
+import { getDateFormatString } from '@ghostfolio/common/helper';
 import { UniqueAsset } from '@ghostfolio/common/interfaces';
 import { OrderWithAccount } from '@ghostfolio/common/types';
 import Big from 'big.js';
@@ -63,7 +63,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
   @ViewChild(MatSort) sort: MatSort;
 
   public dataSource: MatTableDataSource<Activity> = new MatTableDataSource();
-  public defaultDateFormat = DEFAULT_DATE_FORMAT;
+  public defaultDateFormat: string;
   public displayedColumns = [];
   public endOfToday = endOfToday();
   public filters$: Subject<string[]> = new BehaviorSubject([]);
@@ -153,6 +153,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
 
     this.isLoading = true;
 
+    this.defaultDateFormat = getDateFormatString(this.locale);
+
     if (this.activities) {
       this.dataSource = new MatTableDataSource(this.activities);
       this.dataSource.filterPredicate = (data, filter) => {
diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts
index daa8d72bc..08fdb413e 100644
--- a/libs/ui/src/lib/value/value.component.ts
+++ b/libs/ui/src/lib/value/value.component.ts
@@ -4,8 +4,8 @@ import {
   Input,
   OnChanges
 } from '@angular/core';
-import { DEFAULT_DATE_FORMAT, locale } from '@ghostfolio/common/config';
-import { format, isDate, parseISO } from 'date-fns';
+import { getLocale } from '@ghostfolio/common/helper';
+import { isDate, parseISO } from 'date-fns';
 import { isNumber } from 'lodash';
 
 @Component({
@@ -21,7 +21,7 @@ export class ValueComponent implements OnChanges {
   @Input() isCurrency = false;
   @Input() isPercent = false;
   @Input() label = '';
-  @Input() locale = locale;
+  @Input() locale = getLocale();
   @Input() position = '';
   @Input() precision: number | undefined;
   @Input() size: 'large' | 'medium' | 'small' = 'small';
@@ -102,10 +102,13 @@ export class ValueComponent implements OnChanges {
 
         try {
           if (isDate(parseISO(this.value))) {
-            this.formattedValue = format(
-              new Date(<string>this.value),
-              DEFAULT_DATE_FORMAT
-            );
+            this.formattedValue = new Date(
+              <string>this.value
+            ).toLocaleDateString(this.locale, {
+              day: '2-digit',
+              month: '2-digit',
+              year: 'numeric'
+            });
           }
         } catch {
           this.formattedValue = this.value;