From 1e3b88add878a83dc36c80fecb3bc06acf948a63 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:09:46 +0200 Subject: [PATCH] Task/localize default account name created during user registration (#7888) * Localize default account name created during user registration * Update changelog --- CHANGELOG.md | 2 + apps/api/src/app/user/user.controller.ts | 8 +++- apps/api/src/app/user/user.service.ts | 8 ++-- apps/api/src/helper/language.helper.spec.ts | 40 +++++++++++++++++++ apps/api/src/helper/language.helper.ts | 24 +++++++++++ .../language-redirect.middleware.ts | 17 ++------ ...r-account-registration-dialog.component.ts | 2 +- libs/common/src/lib/dtos/create-user.dto.ts | 7 ++++ libs/common/src/lib/dtos/index.ts | 2 + libs/ui/src/lib/services/data.service.ts | 5 ++- 10 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 apps/api/src/helper/language.helper.spec.ts create mode 100644 apps/api/src/helper/language.helper.ts create mode 100644 libs/common/src/lib/dtos/create-user.dto.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 52578ccf02..c19f0f38ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Localized the default account name created during user registration +- Extended the `POST api/v1/user` endpoint by the language code - Improved the language localization for German (`de`) - Upgraded `@internationalized/number` from version `3.6.7` to `3.6.8` - Upgraded `bull-board` from version `9.8.0` to `9.9.0` diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index cf9043910a..ae9e7d680b 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -15,6 +15,7 @@ import { THROTTLE_SIGNUP_TTL } from '@ghostfolio/common/config'; import { + CreateUserDto, DeleteOwnUserDto, UpdateOwnAccessTokenDto, UpdateUserSettingDto @@ -147,7 +148,7 @@ export class UserController { } }) @UseGuards(CustomThrottlerGuard) - public async signupUser(): Promise { + public async signupUser(@Body() data: CreateUserDto): Promise { const isUserSignupEnabled = await this.propertyService.isUserSignupEnabled(); @@ -158,7 +159,10 @@ export class UserController { ); } - const { accessToken, id, role } = await this.userService.createUser(); + const { accessToken, id, role } = await this.userService.createUser({ + data: {}, + languageCode: data.languageCode + }); return { accessToken, diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index d53b3891ac..dae4463d23 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -2,6 +2,7 @@ import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.ser import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { environment } from '@ghostfolio/api/environments/environment'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; +import { getSupportedLanguageCode } from '@ghostfolio/api/helper/language.helper'; import { getRandomString } from '@ghostfolio/api/helper/string.helper'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; @@ -28,7 +29,6 @@ import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { DEFAULT_CURRENCY, DEFAULT_DATE_RANGE, - DEFAULT_LANGUAGE_CODE, DEFAULT_LOCALE, PROPERTY_API_KEY_GHOSTFOLIO, PROPERTY_IS_READ_ONLY_MODE, @@ -692,9 +692,11 @@ export class UserService { public async createUser( { - data + data, + languageCode }: { data: Prisma.UserCreateInput; + languageCode?: string; } = { data: {} } ): Promise { if (!data.provider) { @@ -715,7 +717,7 @@ export class UserService { currency: DEFAULT_CURRENCY, name: this.i18nService.getTranslation({ id: 'myAccount', - languageCode: DEFAULT_LANGUAGE_CODE // TODO + languageCode: getSupportedLanguageCode(languageCode) }) } }, diff --git a/apps/api/src/helper/language.helper.spec.ts b/apps/api/src/helper/language.helper.spec.ts new file mode 100644 index 0000000000..f3159b98ac --- /dev/null +++ b/apps/api/src/helper/language.helper.spec.ts @@ -0,0 +1,40 @@ +import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; + +import { + getLanguageCodeFromHeader, + getSupportedLanguageCode +} from './language.helper'; + +describe('getLanguageCodeFromHeader', () => { + it('should get the primary language code', () => { + expect(getLanguageCodeFromHeader('de-CH,de;q=0.9,en;q=0.8')).toEqual('de'); + }); + + it('should use the default language code if the header is missing', () => { + expect(getLanguageCodeFromHeader()).toEqual(DEFAULT_LANGUAGE_CODE); + }); + + it('should use the default language code if the language is not supported', () => { + expect(getLanguageCodeFromHeader('xx')).toEqual(DEFAULT_LANGUAGE_CODE); + }); +}); + +describe('getSupportedLanguageCode', () => { + it('should return a supported language code', () => { + expect(getSupportedLanguageCode('de')).toEqual('de'); + }); + + it('should ignore the region subtag', () => { + expect(getSupportedLanguageCode('de-CH')).toEqual('de'); + }); + + it('should ignore the case', () => { + expect(getSupportedLanguageCode('DE')).toEqual('de'); + }); + + it('should use the default language code if the language is not supported', () => { + expect(getSupportedLanguageCode()).toEqual(DEFAULT_LANGUAGE_CODE); + expect(getSupportedLanguageCode('')).toEqual(DEFAULT_LANGUAGE_CODE); + expect(getSupportedLanguageCode('xx')).toEqual(DEFAULT_LANGUAGE_CODE); + }); +}); diff --git a/apps/api/src/helper/language.helper.ts b/apps/api/src/helper/language.helper.ts new file mode 100644 index 0000000000..29de5147f6 --- /dev/null +++ b/apps/api/src/helper/language.helper.ts @@ -0,0 +1,24 @@ +import { + DEFAULT_LANGUAGE_CODE, + SUPPORTED_LANGUAGE_CODES +} from '@ghostfolio/common/config'; + +type SupportedLanguageCode = (typeof SUPPORTED_LANGUAGE_CODES)[number]; + +export function getLanguageCodeFromHeader( + acceptLanguage?: string +): SupportedLanguageCode { + return getSupportedLanguageCode(acceptLanguage?.split(',')[0]); +} + +export function getSupportedLanguageCode( + languageCode?: string +): SupportedLanguageCode { + const languageCodeToUse = languageCode + ?.split('-')[0] + .toLowerCase() as SupportedLanguageCode; + + return SUPPORTED_LANGUAGE_CODES.includes(languageCodeToUse) + ? languageCodeToUse + : DEFAULT_LANGUAGE_CODE; +} diff --git a/apps/api/src/middlewares/language-redirect.middleware.ts b/apps/api/src/middlewares/language-redirect.middleware.ts index 5b6fac6c43..82b59db0cd 100644 --- a/apps/api/src/middlewares/language-redirect.middleware.ts +++ b/apps/api/src/middlewares/language-redirect.middleware.ts @@ -1,8 +1,5 @@ import { environment } from '@ghostfolio/api/environments/environment'; -import { - DEFAULT_LANGUAGE_CODE, - SUPPORTED_LANGUAGE_CODES -} from '@ghostfolio/common/config'; +import { getLanguageCodeFromHeader } from '@ghostfolio/api/helper/language.helper'; import { NextFunction, Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; @@ -20,15 +17,9 @@ export function languageRedirectMiddleware( return next(); } - let languageCode = DEFAULT_LANGUAGE_CODE; - - try { - const code = request.headers['accept-language'].split(',')[0].split('-')[0]; - - if ((SUPPORTED_LANGUAGE_CODES as readonly string[]).includes(code)) { - languageCode = code; - } - } catch {} + const languageCode = getLanguageCodeFromHeader( + request.headers['accept-language'] + ); return response.redirect( StatusCodes.MOVED_PERMANENTLY, diff --git a/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts b/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts index 0265357bf5..dc7c7676ad 100644 --- a/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts +++ b/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts @@ -76,7 +76,7 @@ export class GfUserAccountRegistrationDialogComponent { protected createAccount() { this.dataService - .postUser() + .postUser({ languageCode: document.documentElement.lang }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ accessToken, authToken, role }) => { this.accessToken = accessToken; diff --git a/libs/common/src/lib/dtos/create-user.dto.ts b/libs/common/src/lib/dtos/create-user.dto.ts new file mode 100644 index 0000000000..6bc56ca191 --- /dev/null +++ b/libs/common/src/lib/dtos/create-user.dto.ts @@ -0,0 +1,7 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class CreateUserDto { + @IsOptional() + @IsString() + languageCode?: string; +} diff --git a/libs/common/src/lib/dtos/index.ts b/libs/common/src/lib/dtos/index.ts index a679af2492..1dae2b865a 100644 --- a/libs/common/src/lib/dtos/index.ts +++ b/libs/common/src/lib/dtos/index.ts @@ -10,6 +10,7 @@ import { CreateAssetProfileDto } from './create-asset-profile.dto'; import { CreateOrderDto } from './create-order.dto'; import { CreatePlatformDto } from './create-platform.dto'; import { CreateTagDto } from './create-tag.dto'; +import { CreateUserDto } from './create-user.dto'; import { CreateWatchlistItemDto } from './create-watchlist-item.dto'; import { DeleteOwnUserDto } from './delete-own-user.dto'; import { HoldingDto } from './holding.dto'; @@ -43,6 +44,7 @@ export { CreateOrderDto, CreatePlatformDto, CreateTagDto, + CreateUserDto, CreateWatchlistItemDto, DeleteOwnUserDto, HoldingDto, diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index 5f86b7a328..2b390df3c5 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -4,6 +4,7 @@ import { CreateAccountDto, CreateOrderDto, CreateTagDto, + CreateUserDto, CreateWatchlistItemDto, DeleteOwnUserDto, TransferBalanceDto, @@ -896,8 +897,8 @@ export class DataService { return this.http.post(`/api/v1/tags`, aTag); } - public postUser() { - return this.http.post('/api/v1/user', {}); + public postUser(aData: CreateUserDto) { + return this.http.post('/api/v1/user', aData); } public postWatchlistItem(watchlistItem: CreateWatchlistItemDto) {