Browse Source

Merge branch 'ghostfolio:main' into feat/esp-lang-update

pull/4833/head
Terry Casper 3 months ago
committed by GitHub
parent
commit
1e993ec3c1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      .env.dev
  2. 2
      .gitignore
  3. 16
      CHANGELOG.md
  4. 1
      Dockerfile
  5. 14
      apps/api/src/app/auth/jwt.strategy.ts
  6. 10
      apps/api/src/app/endpoints/ai/ai.controller.ts
  7. 2
      apps/api/src/app/portfolio/portfolio.service.ts
  8. 2
      apps/api/src/app/subscription/subscription.service.ts
  9. 2
      apps/api/src/app/user/user.service.ts
  10. 15
      apps/api/src/models/rule.ts
  11. 7
      apps/api/src/models/rules/account-cluster-risk/current-investment.ts
  12. 7
      apps/api/src/models/rules/account-cluster-risk/single-account.ts
  13. 7
      apps/api/src/models/rules/asset-class-cluster-risk/equity.ts
  14. 7
      apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts
  15. 7
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  16. 7
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  17. 7
      apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts
  18. 7
      apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts
  19. 24
      apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts
  20. 36
      apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts
  21. 7
      apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts
  22. 7
      apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts
  23. 7
      apps/api/src/models/rules/regional-market-cluster-risk/europe.ts
  24. 7
      apps/api/src/models/rules/regional-market-cluster-risk/japan.ts
  25. 7
      apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts
  26. 12
      apps/api/src/services/i18n/i18n.service.ts
  27. 7
      apps/client-e2e/project.json
  28. 109
      apps/client/project.json
  29. 3
      apps/client/src/app/components/access-table/access-table.component.ts
  30. 10
      apps/client/src/app/components/admin-settings/admin-settings.component.ts
  31. 16
      apps/client/src/app/pages/i18n/i18n-page.html
  32. 1
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  33. 9
      apps/ui-e2e/project.json
  34. 3
      nx.json
  35. 1811
      package-lock.json
  36. 26
      package.json

1
.env.dev

@ -22,4 +22,3 @@ JWT_SECRET_KEY=<INSERT_RANDOM_STRING>
# For more info, see: https://nx.dev/concepts/inferred-tasks # For more info, see: https://nx.dev/concepts/inferred-tasks
NX_ADD_PLUGINS=false NX_ADD_PLUGINS=false
NX_NATIVE_COMMAND_RUNNER=false

2
.gitignore

@ -25,8 +25,10 @@ npm-debug.log
# misc # misc
/.angular/cache /.angular/cache
.cursor/rules/nx-rules.mdc
.env .env
.env.prod .env.prod
.github/instructions/nx.instructions.md
.nx/cache .nx/cache
.nx/workspace-data .nx/workspace-data
/.sass-cache /.sass-cache

16
CHANGELOG.md

@ -5,6 +5,22 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Set up the language localization for the static portfolio analysis rule: _Emergency Fund_ (Setup)
- Set up the language localization for the static portfolio analysis rule: _Fees_ (Fee Ratio)
### Changed
- Upgraded `ng-extract-i18n-merge` from version `2.15.0` to `2.15.1`
- Upgraded `Nx` from version `20.8.1` to `21.1.2`
### Fixed
- Fixed an issue where the import button was not correctly enabled in the import activities dialog
## 2.166.0 - 2025-06-05 ## 2.166.0 - 2025-06-05
### Added ### Added

1
Dockerfile

@ -33,6 +33,7 @@ COPY ./nx.json nx.json
COPY ./replace.build.mjs replace.build.mjs COPY ./replace.build.mjs replace.build.mjs
COPY ./tsconfig.base.json tsconfig.base.json COPY ./tsconfig.base.json tsconfig.base.json
ENV NX_DAEMON=false
RUN npm run build:production RUN npm run build:production
# Prepare the dist image with additional node_modules # Prepare the dist image with additional node_modules

14
apps/api/src/app/auth/jwt.strategy.ts

@ -1,7 +1,11 @@
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config'; import {
DEFAULT_CURRENCY,
DEFAULT_LANGUAGE_CODE,
HEADER_KEY_TIMEZONE
} from '@ghostfolio/common/config';
import { hasRole } from '@ghostfolio/common/permissions'; import { hasRole } from '@ghostfolio/common/permissions';
import { HttpException, Injectable } from '@nestjs/common'; import { HttpException, Injectable } from '@nestjs/common';
@ -52,6 +56,14 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
}); });
} }
if (!user.Settings.settings.baseCurrency) {
user.Settings.settings.baseCurrency = DEFAULT_CURRENCY;
}
if (!user.Settings.settings.language) {
user.Settings.settings.language = DEFAULT_LANGUAGE_CODE;
}
return user; return user;
} else { } else {
throw new HttpException( throw new HttpException(

10
apps/api/src/app/endpoints/ai/ai.controller.ts

@ -1,10 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import {
DEFAULT_CURRENCY,
DEFAULT_LANGUAGE_CODE
} from '@ghostfolio/common/config';
import { AiPromptResponse } from '@ghostfolio/common/interfaces'; import { AiPromptResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { AiPromptMode, RequestWithUser } from '@ghostfolio/common/types'; import type { AiPromptMode, RequestWithUser } from '@ghostfolio/common/types';
@ -53,10 +49,8 @@ export class AiController {
filters, filters,
mode, mode,
impersonationId: undefined, impersonationId: undefined,
languageCode: languageCode: this.request.user.Settings.settings.language,
this.request.user.Settings.settings.language ?? DEFAULT_LANGUAGE_CODE, userCurrency: this.request.user.Settings.settings.baseCurrency,
userCurrency:
this.request.user.Settings.settings.baseCurrency ?? DEFAULT_CURRENCY,
userId: this.request.user.id userId: this.request.user.id
}); });

2
apps/api/src/app/portfolio/portfolio.service.ts

@ -1319,6 +1319,7 @@ export class PortfolioService {
[ [
new EmergencyFundSetup( new EmergencyFundSetup(
this.exchangeRateDataService, this.exchangeRateDataService,
userSettings.language,
this.getTotalEmergencyFund({ this.getTotalEmergencyFund({
userSettings, userSettings,
emergencyFundHoldingsValueInBaseCurrency: emergencyFundHoldingsValueInBaseCurrency:
@ -1332,6 +1333,7 @@ export class PortfolioService {
[ [
new FeeRatioInitialInvestment( new FeeRatioInitialInvestment(
this.exchangeRateDataService, this.exchangeRateDataService,
userSettings.language,
summary.committedFunds, summary.committedFunds,
summary.fees summary.fees
) )

2
apps/api/src/app/subscription/subscription.service.ts

@ -61,7 +61,7 @@ export class SubscriptionService {
const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = { const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = {
cancel_url: `${this.configurationService.get('ROOT_URL')}/${ cancel_url: `${this.configurationService.get('ROOT_URL')}/${
user.Settings?.settings?.language ?? DEFAULT_LANGUAGE_CODE user.Settings.settings.language
}/account`, }/account`,
client_reference_id: user.id, client_reference_id: user.id,
line_items: [ line_items: [

2
apps/api/src/app/user/user.service.ts

@ -298,10 +298,12 @@ export class UserService {
undefined undefined
).getSettings(user.Settings.settings), ).getSettings(user.Settings.settings),
EmergencyFundSetup: new EmergencyFundSetup( EmergencyFundSetup: new EmergencyFundSetup(
undefined,
undefined, undefined,
undefined undefined
).getSettings(user.Settings.settings), ).getSettings(user.Settings.settings),
FeeRatioInitialInvestment: new FeeRatioInitialInvestment( FeeRatioInitialInvestment: new FeeRatioInitialInvestment(
undefined,
undefined, undefined,
undefined, undefined,
undefined undefined

15
apps/api/src/models/rule.ts

@ -1,5 +1,6 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { groupBy } from '@ghostfolio/common/helper'; import { groupBy } from '@ghostfolio/common/helper';
import { import {
PortfolioPosition, PortfolioPosition,
@ -14,28 +15,28 @@ import { RuleInterface } from './interfaces/rule.interface';
export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> { export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
private key: string; private key: string;
private name: string; private languageCode: string;
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
{ {
key, key,
name languageCode = DEFAULT_LANGUAGE_CODE
}: { }: {
key: string; key: string;
name: string; languageCode?: string; // TODO: Make mandatory
} }
) { ) {
this.key = key; this.key = key;
this.name = name; this.languageCode = languageCode;
} }
public getKey() { public getKey() {
return this.key; return this.key;
} }
public getName() { public getLanguageCode() {
return this.name; return this.languageCode;
} }
public groupCurrentHoldingsByAttribute( public groupCurrentHoldingsByAttribute(
@ -73,5 +74,7 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
PortfolioReportRule['configuration'] PortfolioReportRule['configuration']
>; >;
public abstract getName(): string;
public abstract getSettings(aUserSettings: UserSettings): T; public abstract getSettings(aUserSettings: UserSettings): T;
} }

7
apps/api/src/models/rules/account-cluster-risk/current-investment.ts

@ -15,8 +15,7 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
accounts: PortfolioDetails['accounts'] accounts: PortfolioDetails['accounts']
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: AccountClusterRiskCurrentInvestment.name, key: AccountClusterRiskCurrentInvestment.name
name: 'Investment'
}); });
this.accounts = accounts; this.accounts = accounts;
@ -88,6 +87,10 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
}; };
} }
public getName() {
return 'Investment';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/account-cluster-risk/single-account.ts

@ -11,8 +11,7 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
accounts: PortfolioDetails['accounts'] accounts: PortfolioDetails['accounts']
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: AccountClusterRiskSingleAccount.name, key: AccountClusterRiskSingleAccount.name
name: 'Single Account'
}); });
this.accounts = accounts; this.accounts = accounts;
@ -38,6 +37,10 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
return undefined; return undefined;
} }
public getName() {
return 'Single Account';
}
public getSettings({ xRayRules }: UserSettings): RuleSettings { public getSettings({ xRayRules }: UserSettings): RuleSettings {
return { return {
isActive: xRayRules?.[this.getKey()].isActive ?? true isActive: xRayRules?.[this.getKey()].isActive ?? true

7
apps/api/src/models/rules/asset-class-cluster-risk/equity.ts

@ -11,8 +11,7 @@ export class AssetClassClusterRiskEquity extends Rule<Settings> {
holdings: PortfolioPosition[] holdings: PortfolioPosition[]
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: AssetClassClusterRiskEquity.name, key: AssetClassClusterRiskEquity.name
name: 'Equity'
}); });
this.holdings = holdings; this.holdings = holdings;
@ -78,6 +77,10 @@ export class AssetClassClusterRiskEquity extends Rule<Settings> {
}; };
} }
public getName() {
return 'Equity';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts

@ -11,8 +11,7 @@ export class AssetClassClusterRiskFixedIncome extends Rule<Settings> {
holdings: PortfolioPosition[] holdings: PortfolioPosition[]
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: AssetClassClusterRiskFixedIncome.name, key: AssetClassClusterRiskFixedIncome.name
name: 'Fixed Income'
}); });
this.holdings = holdings; this.holdings = holdings;
@ -78,6 +77,10 @@ export class AssetClassClusterRiskFixedIncome extends Rule<Settings> {
}; };
} }
public getName() {
return 'Fixed Income';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts

@ -11,8 +11,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
holdings: PortfolioPosition[] holdings: PortfolioPosition[]
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: CurrencyClusterRiskBaseCurrencyCurrentInvestment.name, key: CurrencyClusterRiskBaseCurrencyCurrentInvestment.name
name: 'Investment: Base Currency'
}); });
this.holdings = holdings; this.holdings = holdings;
@ -68,6 +67,10 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
return undefined; return undefined;
} }
public getName() {
return 'Investment: Base Currency';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/currency-cluster-risk/current-investment.ts

@ -11,8 +11,7 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
holdings: PortfolioPosition[] holdings: PortfolioPosition[]
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: CurrencyClusterRiskCurrentInvestment.name, key: CurrencyClusterRiskCurrentInvestment.name
name: 'Investment'
}); });
this.holdings = holdings; this.holdings = holdings;
@ -73,6 +72,10 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
}; };
} }
public getName() {
return 'Investment';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts

@ -13,8 +13,7 @@ export class EconomicMarketClusterRiskDevelopedMarkets extends Rule<Settings> {
developedMarketsValueInBaseCurrency: number developedMarketsValueInBaseCurrency: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: EconomicMarketClusterRiskDevelopedMarkets.name, key: EconomicMarketClusterRiskDevelopedMarkets.name
name: 'Developed Markets'
}); });
this.currentValueInBaseCurrency = currentValueInBaseCurrency; this.currentValueInBaseCurrency = currentValueInBaseCurrency;
@ -67,6 +66,10 @@ export class EconomicMarketClusterRiskDevelopedMarkets extends Rule<Settings> {
}; };
} }
public getName() {
return 'Developed Markets';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts

@ -13,8 +13,7 @@ export class EconomicMarketClusterRiskEmergingMarkets extends Rule<Settings> {
emergingMarketsValueInBaseCurrency: number emergingMarketsValueInBaseCurrency: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: EconomicMarketClusterRiskEmergingMarkets.name, key: EconomicMarketClusterRiskEmergingMarkets.name
name: 'Emerging Markets'
}); });
this.currentValueInBaseCurrency = currentValueInBaseCurrency; this.currentValueInBaseCurrency = currentValueInBaseCurrency;
@ -67,6 +66,10 @@ export class EconomicMarketClusterRiskEmergingMarkets extends Rule<Settings> {
}; };
} }
public getName() {
return 'Emerging Markets';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

24
apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts

@ -1,18 +1,21 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule'; import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
import { UserSettings } from '@ghostfolio/common/interfaces'; import { UserSettings } from '@ghostfolio/common/interfaces';
export class EmergencyFundSetup extends Rule<Settings> { export class EmergencyFundSetup extends Rule<Settings> {
private emergencyFund: number; private emergencyFund: number;
private i18nService = new I18nService();
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
languageCode: string,
emergencyFund: number emergencyFund: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: EmergencyFundSetup.name, languageCode,
name: 'Emergency Fund: Set up' key: EmergencyFundSetup.name
}); });
this.emergencyFund = emergencyFund; this.emergencyFund = emergencyFund;
@ -21,13 +24,19 @@ export class EmergencyFundSetup extends Rule<Settings> {
public evaluate() { public evaluate() {
if (!this.emergencyFund) { if (!this.emergencyFund) {
return { return {
evaluation: 'No emergency fund has been set up', evaluation: this.i18nService.getTranslation({
id: 'rule.emergencyFundSetup.false',
languageCode: this.getLanguageCode()
}),
value: false value: false
}; };
} }
return { return {
evaluation: 'An emergency fund has been set up', evaluation: this.i18nService.getTranslation({
id: 'rule.emergencyFundSetup.true',
languageCode: this.getLanguageCode()
}),
value: true value: true
}; };
} }
@ -36,6 +45,13 @@ export class EmergencyFundSetup extends Rule<Settings> {
return undefined; return undefined;
} }
public getName() {
return this.i18nService.getTranslation({
id: 'rule.emergencyFundSetup',
languageCode: this.getLanguageCode()
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

36
apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts

@ -1,20 +1,23 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { Rule } from '@ghostfolio/api/models/rule'; import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
import { UserSettings } from '@ghostfolio/common/interfaces'; import { UserSettings } from '@ghostfolio/common/interfaces';
export class FeeRatioInitialInvestment extends Rule<Settings> { export class FeeRatioInitialInvestment extends Rule<Settings> {
private fees: number; private fees: number;
private i18nService = new I18nService();
private totalInvestment: number; private totalInvestment: number;
public constructor( public constructor(
protected exchangeRateDataService: ExchangeRateDataService, protected exchangeRateDataService: ExchangeRateDataService,
languageCode: string,
totalInvestment: number, totalInvestment: number,
fees: number fees: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: FeeRatioInitialInvestment.name, languageCode,
name: 'Fee Ratio' key: FeeRatioInitialInvestment.name
}); });
this.fees = fees; this.fees = fees;
@ -28,17 +31,27 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
if (feeRatio > ruleSettings.thresholdMax) { if (feeRatio > ruleSettings.thresholdMax) {
return { return {
evaluation: `The fees do exceed ${ evaluation: this.i18nService.getTranslation({
ruleSettings.thresholdMax * 100 id: 'rule.feeRatioInitialInvestment.false',
}% of your initial investment (${(feeRatio * 100).toPrecision(3)}%)`, languageCode: this.getLanguageCode(),
placeholders: {
feeRatio: (ruleSettings.thresholdMax * 100).toFixed(2),
thresholdMax: (feeRatio * 100).toPrecision(3)
}
}),
value: false value: false
}; };
} }
return { return {
evaluation: `The fees do not exceed ${ evaluation: this.i18nService.getTranslation({
ruleSettings.thresholdMax * 100 id: 'rule.feeRatioInitialInvestment.true',
}% of your initial investment (${(feeRatio * 100).toPrecision(3)}%)`, languageCode: this.getLanguageCode(),
placeholders: {
feeRatio: (feeRatio * 100).toPrecision(3),
thresholdMax: (ruleSettings.thresholdMax * 100).toFixed(2)
}
}),
value: true value: true
}; };
} }
@ -55,6 +68,13 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
}; };
} }
public getName() {
return this.i18nService.getTranslation({
id: 'rule.feeRatioInitialInvestment',
languageCode: this.getLanguageCode()
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts

@ -14,8 +14,7 @@ export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> {
asiaPacificValueInBaseCurrency: number asiaPacificValueInBaseCurrency: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: RegionalMarketClusterRiskAsiaPacific.name, key: RegionalMarketClusterRiskAsiaPacific.name
name: 'Asia-Pacific'
}); });
this.asiaPacificValueInBaseCurrency = asiaPacificValueInBaseCurrency; this.asiaPacificValueInBaseCurrency = asiaPacificValueInBaseCurrency;
@ -66,6 +65,10 @@ export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> {
}; };
} }
public getName() {
return 'Asia-Pacific';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts

@ -14,8 +14,7 @@ export class RegionalMarketClusterRiskEmergingMarkets extends Rule<Settings> {
emergingMarketsValueInBaseCurrency: number emergingMarketsValueInBaseCurrency: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: RegionalMarketClusterRiskEmergingMarkets.name, key: RegionalMarketClusterRiskEmergingMarkets.name
name: 'Emerging Markets'
}); });
this.currentValueInBaseCurrency = currentValueInBaseCurrency; this.currentValueInBaseCurrency = currentValueInBaseCurrency;
@ -68,6 +67,10 @@ export class RegionalMarketClusterRiskEmergingMarkets extends Rule<Settings> {
}; };
} }
public getName() {
return 'Emerging Markets';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/regional-market-cluster-risk/europe.ts

@ -14,8 +14,7 @@ export class RegionalMarketClusterRiskEurope extends Rule<Settings> {
europeValueInBaseCurrency: number europeValueInBaseCurrency: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: RegionalMarketClusterRiskEurope.name, key: RegionalMarketClusterRiskEurope.name
name: 'Europe'
}); });
this.currentValueInBaseCurrency = currentValueInBaseCurrency; this.currentValueInBaseCurrency = currentValueInBaseCurrency;
@ -66,6 +65,10 @@ export class RegionalMarketClusterRiskEurope extends Rule<Settings> {
}; };
} }
public getName() {
return 'Europe';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/regional-market-cluster-risk/japan.ts

@ -14,8 +14,7 @@ export class RegionalMarketClusterRiskJapan extends Rule<Settings> {
japanValueInBaseCurrency: number japanValueInBaseCurrency: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: RegionalMarketClusterRiskJapan.name, key: RegionalMarketClusterRiskJapan.name
name: 'Japan'
}); });
this.currentValueInBaseCurrency = currentValueInBaseCurrency; this.currentValueInBaseCurrency = currentValueInBaseCurrency;
@ -66,6 +65,10 @@ export class RegionalMarketClusterRiskJapan extends Rule<Settings> {
}; };
} }
public getName() {
return 'Japan';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

7
apps/api/src/models/rules/regional-market-cluster-risk/north-america.ts

@ -14,8 +14,7 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule<Settings> {
northAmericaValueInBaseCurrency: number northAmericaValueInBaseCurrency: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
key: RegionalMarketClusterRiskNorthAmerica.name, key: RegionalMarketClusterRiskNorthAmerica.name
name: 'North America'
}); });
this.currentValueInBaseCurrency = currentValueInBaseCurrency; this.currentValueInBaseCurrency = currentValueInBaseCurrency;
@ -66,6 +65,10 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule<Settings> {
}; };
} }
public getName() {
return 'North America';
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return { return {
baseCurrency, baseCurrency,

12
apps/api/src/services/i18n/i18n.service.ts

@ -15,10 +15,12 @@ export class I18nService {
public getTranslation({ public getTranslation({
id, id,
languageCode languageCode,
placeholders
}: { }: {
id: string; id: string;
languageCode: string; languageCode: string;
placeholders?: Record<string, string | number>;
}): string { }): string {
const $ = this.translations[languageCode]; const $ = this.translations[languageCode];
@ -26,7 +28,7 @@ export class I18nService {
Logger.warn(`Translation not found for locale '${languageCode}'`); Logger.warn(`Translation not found for locale '${languageCode}'`);
} }
const translatedText = $( let translatedText = $(
`trans-unit[id="${id}"] > ${ `trans-unit[id="${id}"] > ${
languageCode === DEFAULT_LANGUAGE_CODE ? 'source' : 'target' languageCode === DEFAULT_LANGUAGE_CODE ? 'source' : 'target'
}` }`
@ -38,6 +40,12 @@ export class I18nService {
); );
} }
if (placeholders) {
for (const [key, value] of Object.entries(placeholders)) {
translatedText = translatedText.replace(`{${key}}`, String(value));
}
}
return translatedText.trim(); return translatedText.trim();
} }

7
apps/client-e2e/project.json

@ -3,12 +3,13 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/client-e2e/src", "sourceRoot": "apps/client-e2e/src",
"projectType": "application", "projectType": "application",
"tags": [],
"implicitDependencies": ["client"],
"targets": { "targets": {
"e2e": { "e2e": {
"executor": "@nx/cypress:cypress", "executor": "@nx/cypress:cypress",
"options": { "options": {
"cypressConfig": "apps/client-e2e/cypress.json", "cypressConfig": "apps/client-e2e/cypress.json",
"tsConfig": "apps/client-e2e/tsconfig.e2e.json",
"devServerTarget": "client:serve" "devServerTarget": "client:serve"
}, },
"configurations": { "configurations": {
@ -17,7 +18,5 @@
} }
} }
} }
}, }
"tags": [],
"implicitDependencies": ["client"]
} }

109
apps/client/project.json

@ -2,13 +2,63 @@
"name": "client", "name": "client",
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application", "projectType": "application",
"sourceRoot": "apps/client/src",
"prefix": "gf",
"i18n": {
"locales": {
"ca": {
"baseHref": "/ca/",
"translation": "apps/client/src/locales/messages.ca.xlf"
},
"de": {
"baseHref": "/de/",
"translation": "apps/client/src/locales/messages.de.xlf"
},
"es": {
"baseHref": "/es/",
"translation": "apps/client/src/locales/messages.es.xlf"
},
"fr": {
"baseHref": "/fr/",
"translation": "apps/client/src/locales/messages.fr.xlf"
},
"it": {
"baseHref": "/it/",
"translation": "apps/client/src/locales/messages.it.xlf"
},
"nl": {
"baseHref": "/nl/",
"translation": "apps/client/src/locales/messages.nl.xlf"
},
"pl": {
"baseHref": "/pl/",
"translation": "apps/client/src/locales/messages.pl.xlf"
},
"pt": {
"baseHref": "/pt/",
"translation": "apps/client/src/locales/messages.pt.xlf"
},
"tr": {
"baseHref": "/tr/",
"translation": "apps/client/src/locales/messages.tr.xlf"
},
"uk": {
"baseHref": "/uk/",
"translation": "apps/client/src/locales/messages.uk.xlf"
},
"zh": {
"baseHref": "/zh/",
"translation": "apps/client/src/locales/messages.zh.xlf"
}
},
"sourceLocale": "en"
},
"tags": [],
"generators": { "generators": {
"@schematics/angular:component": { "@schematics/angular:component": {
"style": "scss" "style": "scss"
} }
}, },
"sourceRoot": "apps/client/src",
"prefix": "gf",
"targets": { "targets": {
"build": { "build": {
"executor": "@nx/angular:webpack-browser", "executor": "@nx/angular:webpack-browser",
@ -211,7 +261,8 @@
"production": { "production": {
"buildTarget": "client:build:production" "buildTarget": "client:build:production"
} }
} },
"continuous": true
}, },
"extract-i18n": { "extract-i18n": {
"executor": "ng-extract-i18n-merge:ng-extract-i18n-merge", "executor": "ng-extract-i18n-merge:ng-extract-i18n-merge",
@ -247,55 +298,5 @@
}, },
"outputs": ["{workspaceRoot}/coverage/apps/client"] "outputs": ["{workspaceRoot}/coverage/apps/client"]
} }
}, }
"i18n": {
"locales": {
"ca": {
"baseHref": "/ca/",
"translation": "apps/client/src/locales/messages.ca.xlf"
},
"de": {
"baseHref": "/de/",
"translation": "apps/client/src/locales/messages.de.xlf"
},
"es": {
"baseHref": "/es/",
"translation": "apps/client/src/locales/messages.es.xlf"
},
"fr": {
"baseHref": "/fr/",
"translation": "apps/client/src/locales/messages.fr.xlf"
},
"it": {
"baseHref": "/it/",
"translation": "apps/client/src/locales/messages.it.xlf"
},
"nl": {
"baseHref": "/nl/",
"translation": "apps/client/src/locales/messages.nl.xlf"
},
"pl": {
"baseHref": "/pl/",
"translation": "apps/client/src/locales/messages.pl.xlf"
},
"pt": {
"baseHref": "/pt/",
"translation": "apps/client/src/locales/messages.pt.xlf"
},
"tr": {
"baseHref": "/tr/",
"translation": "apps/client/src/locales/messages.tr.xlf"
},
"uk": {
"baseHref": "/uk/",
"translation": "apps/client/src/locales/messages.uk.xlf"
},
"zh": {
"baseHref": "/zh/",
"translation": "apps/client/src/locales/messages.zh.xlf"
}
},
"sourceLocale": "en"
},
"tags": []
} }

3
apps/client/src/app/components/access-table/access-table.component.ts

@ -1,6 +1,5 @@
import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type'; import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
import { paths } from '@ghostfolio/common/paths'; import { paths } from '@ghostfolio/common/paths';
@ -54,7 +53,7 @@ export class AccessTableComponent implements OnChanges {
} }
public getPublicUrl(aId: string): string { public getPublicUrl(aId: string): string {
const languageCode = this.user?.settings?.language ?? DEFAULT_LANGUAGE_CODE; const languageCode = this.user.settings.language;
return `${this.baseUrl}/${languageCode}/${paths.public}/${aId}`; return `${this.baseUrl}/${languageCode}/${paths.public}/${aId}`;
} }

10
apps/client/src/app/components/admin-settings/admin-settings.component.ts

@ -3,10 +3,7 @@ import { NotificationService } from '@ghostfolio/client/core/notification/notifi
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import { PROPERTY_API_KEY_GHOSTFOLIO } from '@ghostfolio/common/config';
DEFAULT_LANGUAGE_CODE,
PROPERTY_API_KEY_GHOSTFOLIO
} from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { import {
DataProviderGhostfolioStatusResponse, DataProviderGhostfolioStatusResponse,
@ -70,11 +67,10 @@ export class AdminSettingsComponent implements OnDestroy, OnInit {
this.user = state.user; this.user = state.user;
this.defaultDateFormat = getDateFormatString( this.defaultDateFormat = getDateFormatString(
this.user?.settings?.locale this.user.settings.locale
); );
const languageCode = const languageCode = this.user.settings.language;
this.user?.settings?.language ?? DEFAULT_LANGUAGE_CODE;
this.pricingUrl = `https://ghostfol.io/${languageCode}/${paths.pricing}`; this.pricingUrl = `https://ghostfol.io/${languageCode}/${paths.pricing}`;

16
apps/client/src/app/pages/i18n/i18n-page.html

@ -11,6 +11,22 @@
performance, portfolio, software, stock, trading, wealth, web3 performance, portfolio, software, stock, trading, wealth, web3
</li> </li>
<li i18n="@@myAccount">My Account</li> <li i18n="@@myAccount">My Account</li>
<li i18n="@@rule.emergencyFundSetup">Emergency Fund: Set up</li>
<li i18n="@@rule.emergencyFundSetup.false">
No emergency fund has been set up
</li>
<li i18n="@@rule.emergencyFundSetup.true">
An emergency fund has been set up
</li>
<li i18n="@@rule.feeRatioInitialInvestment">Fee Ratio</li>
<li i18n="@@rule.feeRatioInitialInvestment.false">
The fees do exceed &#123;thresholdMax&#125;% of your initial investment
(&#123;feeRatio&#125;%)
</li>
<li i18n="@@rule.feeRatioInitialInvestment.true">
The fees do not exceed &#123;thresholdMax&#125;% of your initial
investment (&#123;feeRatio&#125;%)
</li>
<li i18n="@@slogan">Open Source Wealth Management Software</li> <li i18n="@@slogan">Open Source Wealth Management Software</li>
</ul> </ul>
</div> </div>

1
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -330,6 +330,7 @@ export class ImportActivitiesDialog implements OnDestroy {
} finally { } finally {
this.importStep = ImportStep.SELECT_ACTIVITIES; this.importStep = ImportStep.SELECT_ACTIVITIES;
this.snackBar.dismiss(); this.snackBar.dismiss();
this.updateSelection(this.activities);
stepper.next(); stepper.next();

9
apps/ui-e2e/project.json

@ -3,13 +3,14 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/ui-e2e/src", "sourceRoot": "apps/ui-e2e/src",
"projectType": "application", "projectType": "application",
"tags": [],
"implicitDependencies": ["ui"],
"targets": { "targets": {
"e2e": { "e2e": {
"executor": "@nx/cypress:cypress", "executor": "@nx/cypress:cypress",
"options": { "options": {
"cypressConfig": "apps/ui-e2e/cypress.json", "cypressConfig": "apps/ui-e2e/cypress.json",
"devServerTarget": "ui:storybook", "devServerTarget": "ui:storybook"
"tsConfig": "apps/ui-e2e/tsconfig.json"
}, },
"configurations": { "configurations": {
"ci": { "ci": {
@ -23,7 +24,5 @@
"lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"] "lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"]
} }
} }
}, }
"tags": [],
"implicitDependencies": ["ui"]
} }

3
nx.json

@ -72,6 +72,5 @@
] ]
}, },
"parallel": 1, "parallel": 1,
"defaultBase": "origin/main", "defaultBase": "origin/main"
"useLegacyCache": true
} }

1811
package-lock.json

File diff suppressed because it is too large

26
package.json

@ -118,7 +118,7 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"marked": "15.0.4", "marked": "15.0.4",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"ng-extract-i18n-merge": "2.15.0", "ng-extract-i18n-merge": "2.15.1",
"ngx-device-detector": "9.0.0", "ngx-device-detector": "9.0.0",
"ngx-markdown": "19.0.0", "ngx-markdown": "19.0.0",
"ngx-skeleton-loader": "11.0.0", "ngx-skeleton-loader": "11.0.0",
@ -154,17 +154,17 @@
"@eslint/js": "9.24.0", "@eslint/js": "9.24.0",
"@nestjs/schematics": "11.0.5", "@nestjs/schematics": "11.0.5",
"@nestjs/testing": "11.1.0", "@nestjs/testing": "11.1.0",
"@nx/angular": "20.8.1", "@nx/angular": "21.1.2",
"@nx/cypress": "20.8.1", "@nx/cypress": "21.1.2",
"@nx/eslint-plugin": "20.8.1", "@nx/eslint-plugin": "21.1.2",
"@nx/jest": "20.8.1", "@nx/jest": "21.1.2",
"@nx/js": "20.8.1", "@nx/js": "21.1.2",
"@nx/module-federation": "20.8.1", "@nx/module-federation": "21.1.2",
"@nx/nest": "20.8.1", "@nx/nest": "21.1.2",
"@nx/node": "20.8.1", "@nx/node": "21.1.2",
"@nx/storybook": "20.8.1", "@nx/storybook": "21.1.2",
"@nx/web": "20.8.1", "@nx/web": "21.1.2",
"@nx/workspace": "20.8.1", "@nx/workspace": "21.1.2",
"@schematics/angular": "19.2.1", "@schematics/angular": "19.2.1",
"@storybook/addon-essentials": "8.6.12", "@storybook/addon-essentials": "8.6.12",
"@storybook/addon-interactions": "8.6.12", "@storybook/addon-interactions": "8.6.12",
@ -191,7 +191,7 @@
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.4.2", "jest-preset-angular": "14.4.2",
"nx": "20.8.1", "nx": "21.1.2",
"prettier": "3.5.3", "prettier": "3.5.3",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.8.2", "prisma": "6.8.2",

Loading…
Cancel
Save