From d988552adc064a2a2ad056e7529c2d70416d24dd Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Mon, 4 Apr 2022 21:18:45 +0200 Subject: [PATCH] Add locale to account settings --- .../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 +++++++++------- .../portfolio-performance.component.ts | 7 +++++- .../pages/account/account-page.component.ts | 23 +++++++++++++++++ .../src/app/pages/account/account-page.html | 25 +++++++++++++++++++ libs/common/src/lib/config.ts | 2 +- libs/common/src/lib/helper.ts | 16 ++++++++++++ 8 files changed, 98 insertions(+), 20 deletions(-) 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 { - return this.userService.getUser(this.request.user); + public async getUser( + @Headers('accept-language') acceptLanguage: string + ): Promise { + 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 b85f226a4..6ba6ba5e3 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 { + public async getUser( + { + Account, + alias, + id, + permissions, + Settings, + subscription + }: UserWithSettings, + aLocale?: string + ): Promise { const access = await this.prismaService.access.findMany({ include: { User: true @@ -63,8 +66,8 @@ export class UserService { accounts: Account, settings: { ...(Settings.settings), - locale: (Settings.settings).locale ?? locale, baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY, + locale: (Settings.settings).locale ?? aLocale ?? locale, viewMode: Settings?.viewMode ?? ViewMode.DEFAULT } }; 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 440ba178d..3b0596d34 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -23,6 +23,7 @@ import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { baseCurrency } from '@ghostfolio/common/config'; 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'; @@ -51,6 +52,7 @@ export class AccountPageComponent implements OnDestroy, OnInit { 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; @@ -120,6 +122,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(); } }); @@ -142,6 +147,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 40b81627d..d2ca5705b 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -115,6 +115,31 @@ +
+
+
Locale
+
+ Date and number format +
+
+
+ + + + {{ locale }} + + +
+
View Mode diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index ebe35342e..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 = { diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index bb6f6e94d..2e45d40cd 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -71,6 +71,22 @@ export function getLocale() { : 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