Browse Source

Merge branch 'main' into feature/remove-precious-metals-from-analysis

pull/6968/head
Thomas Kaul 13 hours ago
committed by GitHub
parent
commit
89e611dd16
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 14
      CHANGELOG.md
  2. 4
      apps/api/src/app/activities/activities.module.ts
  3. 57
      apps/api/src/app/activities/activities.service.ts
  4. 38
      apps/api/src/app/admin/admin.controller.ts
  5. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  6. 2
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  7. 17
      apps/api/src/helper/country.helper.ts
  8. 28
      apps/api/src/helper/sector.helper.ts
  9. 39
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  10. 66
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  11. 24
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  12. 1
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  13. 10
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  14. 9
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  15. 15
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  16. 9
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  17. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  18. 13
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  19. 11
      apps/client/src/app/pages/public/public-page.component.ts
  20. 15
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
  21. 770
      apps/client/src/locales/messages.ca.xlf
  22. 770
      apps/client/src/locales/messages.de.xlf
  23. 770
      apps/client/src/locales/messages.es.xlf
  24. 770
      apps/client/src/locales/messages.fr.xlf
  25. 770
      apps/client/src/locales/messages.it.xlf
  26. 770
      apps/client/src/locales/messages.ko.xlf
  27. 770
      apps/client/src/locales/messages.nl.xlf
  28. 770
      apps/client/src/locales/messages.pl.xlf
  29. 770
      apps/client/src/locales/messages.pt.xlf
  30. 770
      apps/client/src/locales/messages.tr.xlf
  31. 770
      apps/client/src/locales/messages.uk.xlf
  32. 734
      apps/client/src/locales/messages.xlf
  33. 770
      apps/client/src/locales/messages.zh.xlf
  34. 15
      libs/common/src/lib/config.ts
  35. 14
      libs/common/src/lib/helper.ts
  36. 228
      libs/common/src/lib/personal-finance-tools.ts
  37. 2
      libs/common/src/lib/types/index.ts
  38. 3
      libs/common/src/lib/types/sector-name.type.ts
  39. 13
      libs/ui/src/lib/account-balances/account-balances.component.ts
  40. 18
      libs/ui/src/lib/fab/fab.component.scss
  41. 49
      libs/ui/src/lib/i18n.ts
  42. 6
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  43. 16
      libs/ui/src/lib/world-map-chart/world-map-chart.component.ts
  44. 4
      package-lock.json
  45. 2
      package.json

14
CHANGELOG.md

@ -7,9 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Removed
### Changed
- Removed precious metals (Commodities) from the analysis data by continents, countries, sectors and markets.
- Prefilled the form in the account balance management with the current cash balance
- Removed precious metals (Commodities) from the analysis data by continents, countries, sectors and markets
## 3.8.0 - 2026-06-07
### Added
@ -17,13 +20,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Harmonized the sector names across the data providers
- Localized the country names
- Localized the sector names
- Centralized the asset profile override logic for manual adjustments
- Improved the styling in the user detail dialog of the admin control panel’s users section
- Prevented the deletion of asset profiles that are currently in use
- Ensured market data is correctly removed when an asset profile with no remaining activities is deleted
- Refactored the backend logging to use the instance-based `Logger`
- Improved the language localization for German (`de`)
- Improved the language localization for Ukrainian (`uk`)
### Fixed
- Prevented the floating action button from overlapping the paginator on mobile
- Fixed an issue where the asset profile override (asset class and asset sub class) was not applied to the data enhancers when gathering asset profiles
- Fixed a layout issue in the asset profile dialog of the admin control panel by truncating long titles

4
apps/api/src/app/activities/activities.module.ts

@ -6,9 +6,11 @@ import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redac
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -23,11 +25,13 @@ import { ActivitiesService } from './activities.service';
exports: [ActivitiesService],
imports: [
ApiModule,
BenchmarkModule,
CacheModule,
DataGatheringQueueModule,
DataProviderModule,
ExchangeRateDataModule,
ImpersonationModule,
MarketDataModule,
PrismaModule,
RedactValuesInResponseModule,
RedisCacheModule,

57
apps/api/src/app/activities/activities.service.ts

@ -4,8 +4,10 @@ import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details
import { AssetProfileChangedEvent } from '@ghostfolio/api/events/asset-profile-changed.event';
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
@ -16,7 +18,10 @@ import {
ghostfolioPrefix,
TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import {
canDeleteAssetProfile,
getAssetProfileIdentifier
} from '@ghostfolio/common/helper';
import {
ActivitiesResponse,
Activity,
@ -48,10 +53,12 @@ export class ActivitiesService {
public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly accountService: AccountService,
private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService,
private readonly eventEmitter: EventEmitter2,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService
) {}
@ -262,7 +269,26 @@ export class ActivitiesService {
activity.symbolProfileId
]);
if (symbolProfile.activitiesCount === 0) {
const benchmarkAssetProfiles =
await this.benchmarkService.getBenchmarkAssetProfiles();
const isBenchmark = benchmarkAssetProfiles.some(({ id }) => {
return id === symbolProfile.id;
});
if (
canDeleteAssetProfile({
isBenchmark,
activitiesCount: symbolProfile.activitiesCount,
symbol: symbolProfile.symbol,
watchedByCount: symbolProfile.watchedByCount
})
) {
await this.marketDataService.deleteMany({
dataSource: symbolProfile.dataSource,
symbol: symbolProfile.symbol
});
await this.symbolProfileService.deleteById(activity.symbolProfileId);
}
@ -308,8 +334,31 @@ export class ActivitiesService {
})
);
for (const { activitiesCount, id } of symbolProfiles) {
if (activitiesCount === 0) {
const benchmarkAssetProfiles =
await this.benchmarkService.getBenchmarkAssetProfiles();
for (const {
activitiesCount,
dataSource,
id,
symbol,
watchedByCount
} of symbolProfiles) {
const isBenchmark = benchmarkAssetProfiles.some(
(benchmarkAssetProfile) => {
return benchmarkAssetProfile.id === id;
}
);
if (
canDeleteAssetProfile({
activitiesCount,
isBenchmark,
symbol,
watchedByCount
})
) {
await this.marketDataService.deleteMany({ dataSource, symbol });
await this.symbolProfileService.deleteById(id);
}
}

38
apps/api/src/app/admin/admin.controller.ts

@ -2,9 +2,11 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
import { DemoService } from '@ghostfolio/api/services/demo/demo.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
@ -16,7 +18,10 @@ import {
UpdateAssetProfileDto,
UpdatePropertyDto
} from '@ghostfolio/common/dtos';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import {
canDeleteAssetProfile,
getAssetProfileIdentifier
} from '@ghostfolio/common/helper';
import {
AdminData,
AdminMarketData,
@ -63,10 +68,12 @@ export class AdminController {
public constructor(
private readonly adminService: AdminService,
private readonly apiService: ApiService,
private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService,
private readonly demoService: DemoService,
private readonly manualService: ManualService,
@Inject(REQUEST) private readonly request: RequestWithUser
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolProfileService: SymbolProfileService
) {}
@Get()
@ -290,6 +297,33 @@ export class AdminController {
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<void> {
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([
{ dataSource, symbol }
]);
if (assetProfile) {
const benchmarkAssetProfiles =
await this.benchmarkService.getBenchmarkAssetProfiles();
const isBenchmark = benchmarkAssetProfiles.some(({ id }) => {
return id === assetProfile.id;
});
if (
!canDeleteAssetProfile({
isBenchmark,
activitiesCount: assetProfile.activitiesCount,
symbol: assetProfile.symbol,
watchedByCount: assetProfile.watchedByCount
})
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
}
return this.adminService.deleteProfileData({ dataSource, symbol });
}

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -116,10 +116,12 @@ describe('PortfolioCalculator', () => {
accountBalanceService,
accountService,
null,
null,
dataProviderService,
null,
exchangeRateDataService,
null,
null,
null
);

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

@ -59,10 +59,12 @@ describe('PortfolioService', () => {
null,
accountService,
null,
null,
dataProviderService,
null,
exchangeRateDataService,
null,
null,
null
);

17
apps/api/src/helper/country.helper.ts

@ -0,0 +1,17 @@
import { countries } from 'countries-list';
export function getCountryCodeByName({
aliases = {},
name
}: {
aliases?: Record<string, string>;
name: string;
}): string {
for (const [code, country] of Object.entries(countries)) {
if (country.name === name || country.name === aliases[name]) {
return code;
}
}
return undefined;
}

28
apps/api/src/helper/sector.helper.ts

@ -0,0 +1,28 @@
import { SECTORS } from '@ghostfolio/common/config';
import { SectorName } from '@ghostfolio/common/types';
import { Logger } from '@nestjs/common';
export function getSectorName({
aliases = {},
name
}: {
aliases?: Record<string, SectorName>;
name: string;
}): SectorName {
if (aliases[name]) {
return aliases[name];
}
if ((SECTORS as readonly string[]).includes(name)) {
return name as SectorName;
}
if (name) {
const logger = new Logger('getSectorName');
logger.warn(`Could not map the sector "${name}" to the ontology`);
}
return 'Other';
}

39
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -1,31 +1,35 @@
import { getCountryCodeByName } from '@ghostfolio/api/helper/country.helper';
import { getSectorName } from '@ghostfolio/api/helper/sector.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { Holding } from '@ghostfolio/common/interfaces';
import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { SectorName } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client';
import { countries } from 'countries-list';
@Injectable()
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
private readonly logger = new Logger(TrackinsightDataEnhancerService.name);
private static baseUrl = 'https://www.trackinsight.com/data-api';
private static countriesMapping = {
'Russian Federation': 'Russia',
USA: 'United States'
};
private static holdingsWeightTreshold = 0.85;
private static sectorsMapping = {
private static sectorsMapping: Record<string, SectorName> = {
'Consumer Discretionary': 'Consumer Cyclical',
'Consumer Defensive': 'Consumer Staples',
'Consumer Staples': 'Consumer Defensive',
Financials: 'Financial Services',
'Health Care': 'Healthcare',
'Information Technology': 'Technology'
'Information Technology': 'Technology',
Materials: 'Basic Materials'
};
private readonly logger = new Logger(TrackinsightDataEnhancerService.name);
public constructor(
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService
@ -117,21 +121,11 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
for (const [name, value] of Object.entries<any>(
holdings?.countries ?? {}
)) {
let countryCode: string;
for (const [code, country] of Object.entries(countries)) {
if (
country.name === name ||
country.name ===
TrackinsightDataEnhancerService.countriesMapping[name]
) {
countryCode = code;
break;
}
}
response.countries.push({
code: countryCode,
code: getCountryCodeByName({
name,
aliases: TrackinsightDataEnhancerService.countriesMapping
}),
weight: value.weight
});
}
@ -165,7 +159,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
holdings?.sectors ?? {}
)) {
response.sectors.push({
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name,
name: getSectorName({
name,
aliases: TrackinsightDataEnhancerService.sectorsMapping
}),
weight: value.weight
});
}

66
apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts

@ -1,12 +1,13 @@
import { getSectorName } from '@ghostfolio/api/helper/sector.helper';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import {
DEFAULT_CURRENCY,
REPLACE_NAME_PARTS,
UNKNOWN_KEY
REPLACE_NAME_PARTS
} from '@ghostfolio/common/config';
import { isCurrency } from '@ghostfolio/common/helper';
import { SectorName } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import {
@ -23,6 +24,20 @@ import type { Price } from 'yahoo-finance2/esm/src/modules/quoteSummary-iface';
@Injectable()
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
private static sectorsMapping: Record<string, SectorName> = {
basic_materials: 'Basic Materials',
communication_services: 'Communication Services',
consumer_cyclical: 'Consumer Cyclical',
consumer_defensive: 'Consumer Defensive',
energy: 'Energy',
financial_services: 'Financial Services',
healthcare: 'Healthcare',
industrials: 'Industrials',
realestate: 'Real Estate',
technology: 'Technology',
utilities: 'Utilities'
};
private readonly logger = new Logger(YahooFinanceDataEnhancerService.name);
private readonly yahooFinance = new YahooFinance({
@ -224,7 +239,10 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
.flatMap((sectorWeighting) => {
return Object.entries(sectorWeighting).map(([sector, weight]) => {
return {
name: this.parseSector(sector),
name: getSectorName({
aliases: YahooFinanceDataEnhancerService.sectorsMapping,
name: sector
}),
weight: weight as number
};
});
@ -331,46 +349,4 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
return { assetClass, assetSubClass };
}
private parseSector(aString: string) {
let sector = UNKNOWN_KEY;
switch (aString) {
case 'basic_materials':
sector = 'Basic Materials';
break;
case 'communication_services':
sector = 'Communication Services';
break;
case 'consumer_cyclical':
sector = 'Consumer Cyclical';
break;
case 'consumer_defensive':
sector = 'Consumer Staples';
break;
case 'energy':
sector = 'Energy';
break;
case 'financial_services':
sector = 'Financial Services';
break;
case 'healthcare':
sector = 'Healthcare';
break;
case 'industrials':
sector = 'Industrials';
break;
case 'realestate':
sector = 'Real Estate';
break;
case 'technology':
sector = 'Technology';
break;
case 'utilities':
sector = 'Utilities';
break;
}
return sector;
}
}

24
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -1,3 +1,4 @@
import { getCountryCodeByName } from '@ghostfolio/api/helper/country.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error';
@ -33,7 +34,6 @@ import {
SymbolProfile
} from '@prisma/client';
import { isISIN } from 'class-validator';
import { countries } from 'countries-list';
import {
addDays,
addYears,
@ -49,14 +49,14 @@ import { uniqBy } from 'lodash';
export class FinancialModelingPrepService
implements DataProviderInterface, OnModuleInit
{
private readonly logger = new Logger(FinancialModelingPrepService.name);
private static countriesMapping = {
'Korea (the Republic of)': 'South Korea',
'Russian Federation': 'Russia',
'Taiwan (Province of China)': 'Taiwan'
};
private readonly logger = new Logger(FinancialModelingPrepService.name);
private apiKey: string;
public constructor(
@ -165,21 +165,11 @@ export class FinancialModelingPrepService
return countryName.toLowerCase() !== 'other';
})
.map(({ country: countryName, weightPercentage }) => {
let countryCode: string;
for (const [code, country] of Object.entries(countries)) {
if (
country.name === countryName ||
country.name ===
FinancialModelingPrepService.countriesMapping[countryName]
) {
countryCode = code;
break;
}
}
return {
code: countryCode,
code: getCountryCodeByName({
aliases: FinancialModelingPrepService.countriesMapping,
name: countryName
}),
weight: parseFloat(weightPercentage.slice(0, -1)) / 100
};
});

1
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -148,6 +148,7 @@
[accountBalances]="accountBalances"
[accountCurrency]="currency"
[accountId]="data.accountId"
[currentBalance]="balance"
[locale]="user?.settings?.locale"
[showActions]="
!data.hasImpersonationId &&

10
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -8,6 +8,7 @@ import { UpdateAssetProfileDto } from '@ghostfolio/common/dtos';
import {
canDeleteAssetProfile,
DATE_FORMAT,
getCountryName,
getCurrencyFromSymbol,
isCurrency
} from '@ghostfolio/common/helper';
@ -224,6 +225,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
value: 'max'
}
];
protected readonly getCountryName = getCountryName;
protected historicalDataItems: LineChartItem[];
protected isBenchmark = false;
protected isDataGatheringEnabled: boolean;
@ -246,6 +248,8 @@ export class GfAssetProfileDialogComponent implements OnInit {
[name: string]: { name: string; value: number };
};
protected readonly translate = translate;
protected user: User;
private benchmarks: Partial<SymbolProfile>[];
@ -367,9 +371,9 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.assetProfile?.countries &&
this.assetProfile.countries.length > 0
) {
for (const { code, name, weight } of this.assetProfile.countries) {
for (const { code, weight } of this.assetProfile.countries) {
this.countries[code] = {
name,
name: getCountryName({ code, locale: this.data.locale }),
value: weight
};
}
@ -381,7 +385,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
) {
for (const { name, weight } of this.assetProfile.sectors) {
this.sectors[name] = {
name,
name: translate(name),
value: weight
};
}

9
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -258,7 +258,7 @@
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.sectors[0].name"
[value]="translate(assetProfile?.sectors[0].name)"
>Sector</gf-value
>
</div>
@ -269,7 +269,12 @@
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.countries[0].name"
[value]="
getCountryName({
code: assetProfile?.countries[0].code,
locale: data.locale
})
"
>Country</gf-value
>
</div>

15
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -6,7 +6,11 @@ import {
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
} from '@ghostfolio/common/config';
import { CreateOrderDto } from '@ghostfolio/common/dtos';
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
import {
DATE_FORMAT,
downloadAsFile,
getCountryName
} from '@ghostfolio/common/helper';
import {
Activity,
DataProviderInfo,
@ -121,6 +125,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
public dividendInBaseCurrencyPrecision = 2;
public dividendYieldPercentWithCurrencyEffect: number;
public feeInBaseCurrency: number;
public getCountryName = getCountryName;
public hasPermissionToCreateOwnTag: boolean;
public hasPermissionToReadMarketDataOfOwnAssetProfile: boolean;
public historicalDataItems: LineChartItem[];
@ -157,6 +162,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
public SymbolProfile: EnhancedSymbolProfile;
public tags: Tag[];
public tagsAvailable: Tag[];
public translate = translate;
public user: User;
public value: number;
@ -433,7 +439,10 @@ export class GfHoldingDetailDialogComponent implements OnInit {
if (SymbolProfile?.countries?.length > 0) {
for (const country of SymbolProfile.countries) {
this.countries[country.code] = {
name: country.name,
name: getCountryName({
code: country.code,
locale: this.data.locale
}),
value: country.weight
};
}
@ -442,7 +451,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
if (SymbolProfile?.sectors?.length > 0) {
for (const sector of SymbolProfile.sectors) {
this.sectors[sector.name] = {
name: sector.name,
name: translate(sector.name),
value: sector.weight
};
}

9
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

@ -262,7 +262,7 @@
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.sectors[0].name"
[value]="translate(SymbolProfile.sectors[0].name)"
>Sector</gf-value
>
</div>
@ -272,7 +272,12 @@
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.countries[0].name"
[value]="
getCountryName({
code: SymbolProfile.countries[0].code,
locale: data.locale
})
"
>Country</gf-value
>
</div>

2
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -1,5 +1,5 @@
<div class="container">
<div class="mb-3 row">
<div class="row">
<div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
<gf-activities-table

13
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -3,7 +3,7 @@ import { AccountDetailDialogParams } from '@ghostfolio/client/components/account
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper';
import { getCountryName, prettifySymbol } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
HoldingWithParents,
@ -356,7 +356,7 @@ export class GfAllocationsPageComponent implements OnInit {
if (position.assetProfile.countries.length > 0) {
for (const country of position.assetProfile.countries) {
const { code, continent, name, weight } = country;
const { code, continent, weight } = country;
if (this.continents[continent]?.value) {
this.continents[continent].value +=
@ -366,7 +366,7 @@ export class GfAllocationsPageComponent implements OnInit {
: position.valueInPercentage);
} else {
this.continents[continent] = {
name: continent,
name: translate(continent),
value:
weight *
(isNumber(position.valueInBaseCurrency)
@ -383,7 +383,10 @@ export class GfAllocationsPageComponent implements OnInit {
: position.valueInPercentage);
} else {
this.countries[code] = {
name,
name: getCountryName({
code,
locale: this.user?.settings?.locale
}),
value:
weight *
(isNumber(position.valueInBaseCurrency)
@ -445,7 +448,7 @@ export class GfAllocationsPageComponent implements OnInit {
: position.valueInPercentage);
} else {
this.sectors[name] = {
name,
name: translate(name),
value:
weight *
(isNumber(position.valueInBaseCurrency)

11
apps/client/src/app/pages/public/public-page.component.ts

@ -1,5 +1,5 @@
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper';
import { getCountryName, prettifySymbol } from '@ghostfolio/common/helper';
import {
InfoItem,
PortfolioPosition,
@ -9,6 +9,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Market } from '@ghostfolio/common/types';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table/activities-table.component';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table/holdings-table.component';
import { translate } from '@ghostfolio/ui/i18n';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.component';
import { DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value';
@ -185,14 +186,14 @@ export class GfPublicPageComponent implements OnInit {
if (position.assetProfile.countries.length > 0) {
for (const country of position.assetProfile.countries) {
const { code, continent, name, weight } = country;
const { code, continent, weight } = country;
if (this.continents[continent]?.value) {
this.continents[continent].value +=
weight * (position.valueInBaseCurrency ?? 0);
} else {
this.continents[continent] = {
name: continent,
name: translate(continent),
value:
weight *
(this.publicPortfolioDetails.holdings[symbol]
@ -205,7 +206,7 @@ export class GfPublicPageComponent implements OnInit {
weight * (position.valueInBaseCurrency ?? 0);
} else {
this.countries[code] = {
name,
name: getCountryName({ code }),
value:
weight *
(this.publicPortfolioDetails.holdings[symbol]
@ -232,7 +233,7 @@ export class GfPublicPageComponent implements OnInit {
weight * (position.valueInBaseCurrency ?? 0);
} else {
this.sectors[name] = {
name,
name: translate(name),
value:
weight *
(this.publicPortfolioDetails.holdings[symbol]

15
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts

@ -1,3 +1,4 @@
import { getCountryName } from '@ghostfolio/common/helper';
import { Product } from '@ghostfolio/common/interfaces';
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
@ -32,6 +33,7 @@ export class GfProductPageComponent implements OnInit {
) {}
public ngOnInit() {
const locale = document.documentElement.lang;
const { subscriptionOffer } = this.dataService.fetchInfo();
this.price = subscriptionOffer?.price;
@ -55,18 +57,23 @@ export class GfProductPageComponent implements OnInit {
'Türkçe'
],
name: 'Ghostfolio',
origin: $localize`Switzerland`,
origin: getCountryName({ locale, code: 'CH' }),
regions: [$localize`Global`],
slogan: 'Open Source Wealth Management',
useAnonymously: true
};
this.product2 = personalFinanceTools.find(({ key }) => {
this.product2 = {
...personalFinanceTools.find(({ key }) => {
return key === this.route.snapshot.data['key'];
});
})
};
if (this.product2.origin) {
this.product2.origin = translate(this.product2.origin);
this.product2.origin = getCountryName({
locale,
code: this.product2.origin
});
}
if (this.product2.regions) {

770
apps/client/src/locales/messages.ca.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.de.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.es.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.fr.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.it.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.ko.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.nl.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.pl.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.pt.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.tr.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.uk.xlf

File diff suppressed because it is too large

734
apps/client/src/locales/messages.xlf

File diff suppressed because it is too large

770
apps/client/src/locales/messages.zh.xlf

File diff suppressed because it is too large

15
libs/common/src/lib/config.ts

@ -282,6 +282,21 @@ export const REPLACE_NAME_PARTS = [
'Xtrackers (IE) Plc -'
];
export const SECTORS = [
'Basic Materials',
'Communication Services',
'Consumer Cyclical',
'Consumer Defensive',
'Energy',
'Financial Services',
'Healthcare',
'Industrials',
'Other',
'Real Estate',
'Technology',
'Utilities'
] as const;
export const STORYBOOK_PATH = '/development/storybook';
export const SUPPORTED_LANGUAGE_CODES = [

14
libs/common/src/lib/helper.ts

@ -258,6 +258,20 @@ export function getCurrencyFromSymbol(aSymbol = '') {
return aSymbol.replace(DEFAULT_CURRENCY, '');
}
export function getCountryName({
code,
locale = getLocale()
}: {
code: string;
locale?: string;
}): string {
try {
return new Intl.DisplayNames([locale], { type: 'region' }).of(code) ?? code;
} catch {
return code;
}
}
export function getDateFnsLocale(aLanguageCode?: string) {
if (aLanguageCode === 'ca') {
return ca;

228
libs/common/src/lib/personal-finance-tools.ts

@ -16,7 +16,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'allvue-systems',
name: 'Allvue Systems',
origin: 'United States',
origin: 'US',
slogan: 'Investment Software Suite',
url: 'https://www.allvuesystems.com'
},
@ -33,7 +33,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'altoo',
name: 'Altoo Wealth Platform',
origin: 'Switzerland',
origin: 'CH',
slogan: 'Simplicity for Complex Wealth',
url: 'https://altoo.io'
},
@ -43,7 +43,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'altruist',
name: 'Altruist',
origin: 'United States',
origin: 'US',
slogan: 'The wealth platform built for independent advisors',
url: 'https://altruist.com'
},
@ -53,7 +53,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'amsflow',
name: 'Amsflow Portfolio',
origin: 'Singapore',
origin: 'SG',
pricingPerYear: '$228',
slogan: 'Portfolio Visualizer',
url: 'https://amsflow.com'
@ -65,7 +65,7 @@ export const personalFinanceTools: Product[] = [
key: 'anlage.app',
languages: ['English'],
name: 'Anlage.App',
origin: 'Austria',
origin: 'AT',
pricingPerYear: '$120',
slogan: 'Analyze and track your portfolio.',
url: 'https://anlage.app'
@ -76,7 +76,7 @@ export const personalFinanceTools: Product[] = [
key: 'asseta',
languages: ['English'],
name: 'Asseta',
origin: 'United States',
origin: 'US',
slogan: 'The Intelligent Family Office Suite',
url: 'https://www.asseta.ai'
},
@ -84,7 +84,7 @@ export const personalFinanceTools: Product[] = [
founded: 2016,
key: 'atominvest',
name: 'Atominvest',
origin: 'United Kingdom',
origin: 'GB',
slogan: 'Portfolio Management',
url: 'https://www.atominvest.co'
},
@ -94,7 +94,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'balance-pro',
name: 'Balance Pro',
origin: 'United States',
origin: 'US',
pricingPerYear: '$47.99',
slogan: 'The Smarter Way to Track Your Finances',
url: 'https://www.balancepro.app'
@ -104,7 +104,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: true,
key: 'banktivity',
name: 'Banktivity',
origin: 'United States',
origin: 'US',
pricingPerYear: '$59.99',
slogan: 'Proactive money management app for macOS & iOS',
url: 'https://www.banktivity.com'
@ -124,7 +124,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'beanvest',
name: 'Beanvest',
origin: 'France',
origin: 'FR',
pricingPerYear: '$100',
slogan: 'Stock Portfolio Tracker for Smart Investors',
url: 'https://beanvest.com'
@ -135,7 +135,7 @@ export const personalFinanceTools: Product[] = [
key: 'bluebudget',
languages: ['Deutsch', 'English', 'Français', 'Italiano'],
name: 'BlueBudget',
origin: 'Switzerland',
origin: 'CH',
slogan: 'Schweizer Budget App für einfache & smarte Budgetplanung',
url: 'https://www.bluebudget.ch'
},
@ -146,7 +146,7 @@ export const personalFinanceTools: Product[] = [
key: 'boldin',
name: 'Boldin',
note: 'Originally named as NewRetirement',
origin: 'United States',
origin: 'US',
pricingPerYear: '$144',
slogan: 'Take control with retirement planning tools that begin with you',
url: 'https://www.boldin.com'
@ -154,7 +154,7 @@ export const personalFinanceTools: Product[] = [
{
key: 'budgetpulse',
name: 'BudgetPulse',
origin: 'United States',
origin: 'US',
slogan: 'Giving life to your finance!',
url: 'https://www.budgetpulse.com'
},
@ -164,7 +164,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'buxfer',
name: 'Buxfer',
origin: 'United States',
origin: 'US',
pricingPerYear: '$48',
regions: ['Global'],
slogan: 'Take control of your financial future',
@ -175,7 +175,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'capitally',
name: 'Capitally',
origin: 'Poland',
origin: 'PL',
pricingPerYear: '€80',
slogan: 'Optimize your investments performance',
url: 'https://www.mycapitally.com'
@ -185,7 +185,7 @@ export const personalFinanceTools: Product[] = [
isArchived: true,
key: 'capmon',
name: 'CapMon.org',
origin: 'Germany',
origin: 'DE',
note: 'CapMon.org was discontinued in 2023',
slogan: 'Next Generation Assets Tracking'
},
@ -202,7 +202,7 @@ export const personalFinanceTools: Product[] = [
founded: 2011,
key: 'cobalt',
name: 'Cobalt',
origin: 'United States',
origin: 'US',
slogan: 'Next-Level Portfolio Monitoring',
url: 'https://www.cobalt.pe'
},
@ -212,7 +212,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'coinstats',
name: 'CoinStats',
origin: 'Armenia',
origin: 'AM',
pricingPerYear: '$168',
slogan: 'Manage All Your Wallets & Exchanges From One Place',
url: 'https://coinstats.app'
@ -224,7 +224,7 @@ export const personalFinanceTools: Product[] = [
key: 'cointracking',
languages: ['Deutsch', 'English'],
name: 'CoinTracking',
origin: 'Germany',
origin: 'DE',
pricingPerYear: '$120',
slogan: 'The leading Crypto Portfolio Tracker & Tax Calculator',
url: 'https://cointracking.info'
@ -233,7 +233,7 @@ export const personalFinanceTools: Product[] = [
founded: 2019,
key: 'compound-planning',
name: 'Compound Planning',
origin: 'United States',
origin: 'US',
slogan: 'Modern Wealth & Investment Management',
url: 'https://compoundplanning.com'
},
@ -243,7 +243,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'copilot-money',
name: 'Copilot Money',
origin: 'United States',
origin: 'US',
pricingPerYear: '$95',
slogan: 'Do money better with Copilot',
url: 'https://www.copilot.money'
@ -253,7 +253,7 @@ export const personalFinanceTools: Product[] = [
hasFreePlan: false,
key: 'countabout',
name: 'CountAbout',
origin: 'United States',
origin: 'US',
pricingPerYear: '$9.99',
slogan: 'Customizable and Secure Personal Finance App',
url: 'https://countabout.com'
@ -263,7 +263,7 @@ export const personalFinanceTools: Product[] = [
hasFreePlan: false,
key: 'danti',
name: 'Danti',
origin: 'United Kingdom',
origin: 'GB',
slogan: 'Digitising Generational Wealth',
url: 'https://danti.io'
},
@ -282,7 +282,7 @@ export const personalFinanceTools: Product[] = [
key: 'defi-portfolio-tracker-by-zerion',
languages: ['English'],
name: 'DeFi Portfolio Tracker by Zerion',
origin: 'United States',
origin: 'US',
pricingPerYear: '$99',
slogan: 'DeFi Portfolio Tracker for All Chains',
url: 'https://zerion.io/defi-portfolio-tracker'
@ -294,7 +294,7 @@ export const personalFinanceTools: Product[] = [
key: 'degiro-portfolio-tracker-by-capitalyse',
languages: ['English'],
name: 'DEGIRO Portfolio Tracker by Capitalyse',
origin: 'Netherlands',
origin: 'NL',
pricingPerYear: '€24',
slogan: 'Democratizing Data Analytics',
url: 'https://capitalyse.app/app/degiro'
@ -306,7 +306,7 @@ export const personalFinanceTools: Product[] = [
key: 'delta',
name: 'Delta Investment Tracker',
note: 'Acquired by eToro',
origin: 'Belgium',
origin: 'BE',
pricingPerYear: '$150',
slogan: 'The app to track all your investments. Make smart moves only.',
url: 'https://delta.app'
@ -328,7 +328,7 @@ export const personalFinanceTools: Product[] = [
key: 'divvydiary',
languages: ['Deutsch', 'English'],
name: 'DivvyDiary',
origin: 'Germany',
origin: 'DE',
pricingPerYear: '€65',
slogan: 'Your personal Dividend Calendar',
url: 'https://divvydiary.com'
@ -339,7 +339,7 @@ export const personalFinanceTools: Product[] = [
key: 'empower',
name: 'Empower',
note: 'Originally named as Personal Capital',
origin: 'United States',
origin: 'US',
slogan: 'Get answers to your money questions',
url: 'https://www.empower.com'
},
@ -348,7 +348,7 @@ export const personalFinanceTools: Product[] = [
founded: 2022,
key: 'eightfigures',
name: '8FIGURES',
origin: 'United States',
origin: 'US',
slogan: 'Portfolio Tracker Designed by Professional Investors',
url: 'https://8figures.com'
},
@ -357,7 +357,7 @@ export const personalFinanceTools: Product[] = [
hasFreePlan: false,
key: 'etops',
name: 'etops',
origin: 'Switzerland',
origin: 'CH',
slogan: 'Your financial superpower',
url: 'https://www.etops.com'
},
@ -367,7 +367,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'exirio',
name: 'Exirio',
origin: 'United States',
origin: 'US',
pricingPerYear: '$100',
slogan: 'All your wealth, in one place.',
url: 'https://www.exirio.com'
@ -378,7 +378,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'fey',
name: 'Fey',
origin: 'Canada',
origin: 'CA',
pricingPerYear: '$300',
slogan: 'Make better investments.',
url: 'https://fey.com'
@ -390,7 +390,7 @@ export const personalFinanceTools: Product[] = [
key: 'fina',
languages: ['English'],
name: 'Fina',
origin: 'United States',
origin: 'US',
pricingPerYear: '$115',
slogan: 'Flexible Financial Management',
url: 'https://www.fina.money'
@ -401,7 +401,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'finanzfluss-copilot',
name: 'Finanzfluss Copilot',
origin: 'Germany',
origin: 'DE',
pricingPerYear: '€69.99',
slogan: 'Portfolio Tracker für dein Vermögen',
url: 'https://www.finanzfluss.de/copilot'
@ -411,7 +411,7 @@ export const personalFinanceTools: Product[] = [
key: 'finary',
languages: ['Deutsch', 'English', 'Français'],
name: 'Finary',
origin: 'United States',
origin: 'US',
slogan: 'Real-Time Portfolio Tracker & Stock Tracker',
url: 'https://finary.com'
},
@ -422,7 +422,7 @@ export const personalFinanceTools: Product[] = [
key: 'finateka',
languages: ['English'],
name: 'FINATEKA',
origin: 'United States',
origin: 'US',
slogan:
'The most convenient mobile application for personal finance accounting',
url: 'https://finateka.com'
@ -431,7 +431,7 @@ export const personalFinanceTools: Product[] = [
founded: 2022,
key: 'fincake',
name: 'Fincake',
origin: 'British Virgin Islands',
origin: 'VG',
slogan: 'Easy-to-use Portfolio Tracker',
url: 'https://fincake.io'
},
@ -440,7 +440,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'finvest',
name: 'Finvest',
origin: 'United States',
origin: 'US',
slogan: 'Grow your wealth in a stress-free way',
url: 'https://www.getfinvest.com'
},
@ -449,7 +449,7 @@ export const personalFinanceTools: Product[] = [
hasFreePlan: true,
key: 'finwise',
name: 'FinWise',
origin: 'South Africa',
origin: 'ZA',
pricingPerYear: '€69.99',
slogan: 'Personal finances, simplified',
url: 'https://finwiseapp.io'
@ -461,7 +461,7 @@ export const personalFinanceTools: Product[] = [
key: 'firekit',
languages: ['English', 'українська мова'],
name: 'FIREkit',
origin: 'Ukraine',
origin: 'UA',
pricingPerYear: '$40',
slogan: 'A simple solution to track your wealth online',
url: 'https://firekit.space'
@ -472,7 +472,7 @@ export const personalFinanceTools: Product[] = [
key: 'folishare',
languages: ['Deutsch', 'English'],
name: 'folishare',
origin: 'Austria',
origin: 'AT',
pricingPerYear: '$65',
slogan: 'Take control over your investments',
url: 'https://www.folishare.com'
@ -490,7 +490,7 @@ export const personalFinanceTools: Product[] = [
'Português'
],
name: 'Gasti',
origin: 'Argentina',
origin: 'AR',
pricingPerYear: '$60',
regions: ['Global'],
slogan: 'Take control of your finances from WhatsApp',
@ -503,7 +503,7 @@ export const personalFinanceTools: Product[] = [
key: 'getquin',
languages: ['Deutsch', 'English'],
name: 'getquin',
origin: 'Germany',
origin: 'DE',
pricingPerYear: '€48',
slogan: 'Portfolio Tracker, Analysis & Community',
url: 'https://www.getquin.com'
@ -515,7 +515,7 @@ export const personalFinanceTools: Product[] = [
key: 'gospatz',
name: 'goSPATZ',
note: 'Renamed to Money Peak',
origin: 'Germany',
origin: 'DE',
slogan: 'Volle Kontrolle über deine Investitionen'
},
{
@ -525,7 +525,7 @@ export const personalFinanceTools: Product[] = [
key: 'gustav',
languages: ['Français'],
name: 'Gustav',
origin: 'France',
origin: 'FR',
pricingPerYear: '€59.99',
slogan: 'Prenez enfin le contrôle de votre argent',
url: 'https://get-gustav.com'
@ -536,7 +536,7 @@ export const personalFinanceTools: Product[] = [
key: 'holistic-capital',
languages: ['Deutsch'],
name: 'Holistic',
origin: 'Germany',
origin: 'DE',
slogan: 'Die All-in-One Lösung für dein Vermögen.',
url: 'https://holistic.capital',
useAnonymously: true
@ -546,7 +546,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'honeydue',
name: 'Honeydue',
origin: 'United States',
origin: 'US',
slogan: 'Finance App for Couples',
url: 'https://www.honeydue.com'
},
@ -556,7 +556,7 @@ export const personalFinanceTools: Product[] = [
languages: ['English'],
name: 'Income Reign',
note: 'Income Reign was discontinued in 2025',
origin: 'United States',
origin: 'US',
pricingPerYear: '$120'
},
{
@ -566,7 +566,7 @@ export const personalFinanceTools: Product[] = [
key: 'intuit-mint',
name: 'Intuit Mint',
note: 'Intuit Mint was discontinued in 2023',
origin: 'United States',
origin: 'US',
pricingPerYear: '$60',
slogan: 'Managing money, made simple'
},
@ -575,7 +575,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'investify',
name: 'Investify',
origin: 'Pakistan',
origin: 'PK',
slogan: 'Advanced portfolio tracking and stock market information',
url: 'https://www.investify.pk'
},
@ -586,7 +586,7 @@ export const personalFinanceTools: Product[] = [
key: 'invmon',
name: 'InvMon',
note: 'Originally named as A2PB',
origin: 'Switzerland',
origin: 'CH',
pricingPerYear: '$156',
slogan: 'Track all your assets, investments and portfolios in one place',
url: 'https://invmon.com',
@ -598,7 +598,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'justetf',
name: 'justETF',
origin: 'Germany',
origin: 'DE',
pricingPerYear: '€119',
slogan: 'ETF portfolios made simple',
url: 'https://www.justetf.com'
@ -609,7 +609,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'koinly',
name: 'Koinly',
origin: 'Singapore',
origin: 'SG',
slogan: 'Track all your crypto wallets in one place',
url: 'https://koinly.io'
},
@ -619,7 +619,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'koyfin',
name: 'Koyfin',
origin: 'United States',
origin: 'US',
pricingPerYear: '$468',
slogan: 'Comprehensive financial data analysis',
url: 'https://www.koyfin.com'
@ -630,7 +630,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'kubera',
name: 'Kubera®',
origin: 'United States',
origin: 'US',
pricingPerYear: '$249',
slogan: 'The Time Machine for your Net Worth',
url: 'https://www.kubera.com'
@ -641,7 +641,7 @@ export const personalFinanceTools: Product[] = [
key: 'leafs',
languages: ['Deutsch', 'English'],
name: 'Leafs',
origin: 'Switzerland',
origin: 'CH',
slogan: 'Sustainability insights for wealth managers',
url: 'https://leafs.ch'
},
@ -651,7 +651,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'magnifi',
name: 'Magnifi',
origin: 'United States',
origin: 'US',
pricingPerYear: '$132',
slogan: 'AI Investing Assistant',
url: 'https://magnifi.com'
@ -663,7 +663,7 @@ export const personalFinanceTools: Product[] = [
key: 'markets.sh',
languages: ['English'],
name: 'markets.sh',
origin: 'Germany',
origin: 'DE',
pricingPerYear: '€168',
regions: ['Global'],
slogan: 'Track your investments',
@ -673,7 +673,7 @@ export const personalFinanceTools: Product[] = [
founded: 2010,
key: 'masttro',
name: 'Masttro',
origin: 'United States',
origin: 'US',
slogan: 'Your platform for wealth in full view',
url: 'https://masttro.com'
},
@ -687,7 +687,7 @@ export const personalFinanceTools: Product[] = [
languages: ['English'],
name: 'Maybe Finance',
note: 'Maybe Finance was discontinued in 2023, relaunched in 2024, and discontinued again in 2025',
origin: 'United States',
origin: 'US',
pricingPerYear: '$145',
regions: ['United States'],
slogan: 'Your financial future, in your control',
@ -699,7 +699,7 @@ export const personalFinanceTools: Product[] = [
key: 'merlincrypto',
languages: ['English'],
name: 'Merlin',
origin: 'United States',
origin: 'US',
pricingPerYear: '$204',
regions: ['Canada', 'United States'],
slogan: 'The smartest way to track your crypto',
@ -712,7 +712,7 @@ export const personalFinanceTools: Product[] = [
key: 'microsoft-money',
name: 'Microsoft Money',
note: 'Microsoft Money was discontinued in 2010',
origin: 'United States'
origin: 'US'
},
{
founded: 2019,
@ -720,7 +720,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'monarch-money',
name: 'Monarch Money',
origin: 'United States',
origin: 'US',
pricingPerYear: '$99.99',
slogan: 'The modern way to manage your money',
url: 'https://www.monarch.com'
@ -731,7 +731,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: true,
key: 'moneydance',
name: 'Moneydance',
origin: 'Scotland',
origin: 'GB',
pricingPerYear: '$100',
slogan: 'Personal Finance Manager for Mac, Windows, and Linux',
url: 'https://moneydance.com'
@ -742,7 +742,7 @@ export const personalFinanceTools: Product[] = [
key: 'moneypeak',
name: 'Money Peak',
note: 'Originally named as goSPATZ',
origin: 'Germany',
origin: 'DE',
slogan: 'Dein smarter Finance Assistant',
url: 'https://moneypeak.ai'
},
@ -751,7 +751,7 @@ export const personalFinanceTools: Product[] = [
key: 'moneyspire',
name: 'Moneyspire',
note: 'License is a perpetual license',
origin: 'United States',
origin: 'US',
pricingPerYear: '$59.99',
slogan: 'Have total control of your financial life',
url: 'https://www.moneyspire.com'
@ -759,7 +759,7 @@ export const personalFinanceTools: Product[] = [
{
key: 'moneywiz',
name: 'MoneyWiz',
origin: 'United States',
origin: 'US',
pricingPerYear: '$29.99',
slogan: 'Get money management superpowers',
url: 'https://www.wiz.money'
@ -780,7 +780,7 @@ export const personalFinanceTools: Product[] = [
key: 'monsy',
languages: ['English'],
name: 'Monsy',
origin: 'Indonesia',
origin: 'ID',
pricingPerYear: '$20',
slogan: 'Smart, simple, stress-free money tracking.',
url: 'https://www.monsy.app'
@ -791,7 +791,7 @@ export const personalFinanceTools: Product[] = [
key: 'morningstar-portfolio-manager',
languages: ['English'],
name: 'Morningstar® Portfolio Manager',
origin: 'United States',
origin: 'US',
slogan:
'Track your equity, fund, investment trust, ETF and pension investments in one place.',
url: 'https://www.morningstar.com/mm'
@ -812,7 +812,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'nansen',
name: 'Crypto Portfolio Tracker by Nansen',
origin: 'Singapore',
origin: 'SG',
pricingPerYear: '$1188',
slogan: 'Your Complete Crypto Portfolio, Reimagined',
url: 'https://www.nansen.ai/crypto-portfolio-tracker'
@ -823,7 +823,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'navexa',
name: 'Navexa',
origin: 'Australia',
origin: 'AU',
pricingPerYear: '$90',
slogan: 'The Intelligent Portfolio Tracker',
url: 'https://www.navexa.com'
@ -844,7 +844,7 @@ export const personalFinanceTools: Product[] = [
key: 'parqet',
name: 'Parqet',
note: 'Originally named as Tresor One',
origin: 'Germany',
origin: 'DE',
pricingPerYear: '€99.99',
regions: ['Austria', 'Germany', 'Switzerland'],
slogan: 'Dein Vermögen immer im Blick',
@ -854,14 +854,14 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'peek',
name: 'Peek',
origin: 'Singapore',
origin: 'SG',
slogan: 'Feel in control of your money without spreadsheets or shame',
url: 'https://peek.money'
},
{
key: 'pennies',
name: 'Pennies',
origin: 'United States',
origin: 'US',
pricingPerYear: '$39.99',
slogan: 'Your money. Made simple.',
url: 'https://www.getpennies.com'
@ -872,7 +872,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'pinklion',
name: 'PinkLion',
origin: 'Germany',
origin: 'DE',
pricingPerYear: '€50',
slogan: 'Invest smarter, not harder',
url: 'https://pinklion.xyz'
@ -884,7 +884,7 @@ export const personalFinanceTools: Product[] = [
key: 'plainzer',
languages: ['English'],
name: 'Plainzer',
origin: 'Poland',
origin: 'PL',
pricingPerYear: '$74',
slogan: 'Free dividend tracker for your portfolio',
url: 'https://plainzer.com'
@ -894,7 +894,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'plannix',
name: 'Plannix',
origin: 'Italy',
origin: 'IT',
slogan: 'Your Personal Finance Hub',
url: 'https://www.plannix.co'
},
@ -904,7 +904,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'pocketguard',
name: 'PocketGuard',
origin: 'United States',
origin: 'US',
pricingPerYear: '$74.99',
slogan: 'Budgeting App & Finance Planner',
url: 'https://pocketguard.com'
@ -916,7 +916,7 @@ export const personalFinanceTools: Product[] = [
key: 'pocketsmith',
languages: ['English'],
name: 'PocketSmith',
origin: 'New Zealand',
origin: 'NZ',
pricingPerYear: '$120',
regions: ['Global'],
slogan: 'Know where your money is going',
@ -928,7 +928,7 @@ export const personalFinanceTools: Product[] = [
key: 'portfolio-dividend-tracker',
languages: ['English', 'Nederlands'],
name: 'Portfolio Dividend Tracker',
origin: 'Netherlands',
origin: 'NL',
pricingPerYear: '€60',
slogan: 'Manage all your portfolios',
url: 'https://portfoliodividendtracker.com'
@ -959,7 +959,7 @@ export const personalFinanceTools: Product[] = [
key: 'portseido',
languages: ['Deutsch', 'English', 'Français', 'Nederlands'],
name: 'Portseido',
origin: 'Thailand',
origin: 'TH',
pricingPerYear: '$96',
slogan: 'Portfolio Performance and Dividend Tracker',
url: 'https://www.portseido.com'
@ -970,7 +970,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: true,
key: 'projectionlab',
name: 'ProjectionLab',
origin: 'United States',
origin: 'US',
pricingPerYear: '$108',
slogan: 'Build Financial Plans You Love.',
url: 'https://projectionlab.com'
@ -981,7 +981,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'prostocktracker',
name: 'Pro Stock Tracker',
origin: 'United Kingdom',
origin: 'GB',
pricingPerYear: '$60',
slogan: 'The stock portfolio tracker built for long-term investors',
url: 'https://prostocktracker.com'
@ -1002,7 +1002,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'rocket-money',
name: 'Rocket Money',
origin: 'United States',
origin: 'US',
slogan: 'Track your net worth',
url: 'https://www.rocketmoney.com'
},
@ -1013,7 +1013,7 @@ export const personalFinanceTools: Product[] = [
key: 'sarmaaya.pk',
name: 'Sarmaaya.pk Portfolio Tracking',
note: 'Sarmaaya.pk Portfolio Tracking was discontinued in 2024',
origin: 'Pakistan',
origin: 'PK',
slogan: 'Unified platform for financial research and portfolio tracking'
},
{
@ -1022,7 +1022,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'seeking-alpha',
name: 'Seeking Alpha',
origin: 'United States',
origin: 'US',
pricingPerYear: '$239',
slogan: 'Stock Market Analysis & Tools for Investors',
url: 'https://seekingalpha.com'
@ -1031,7 +1031,7 @@ export const personalFinanceTools: Product[] = [
founded: 2022,
key: 'segmio',
name: 'Segmio',
origin: 'Romania',
origin: 'RO',
slogan: 'Wealth Management and Net Worth Tracking',
url: 'https://www.segmio.com'
},
@ -1041,7 +1041,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'sharesight',
name: 'Sharesight',
origin: 'New Zealand',
origin: 'NZ',
pricingPerYear: '$135',
regions: ['Global'],
slogan: 'Stock Portfolio Tracker',
@ -1060,7 +1060,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'simple-portfolio',
name: 'Simple Portfolio',
origin: 'Czech Republic',
origin: 'CZ',
pricingPerYear: '€80',
slogan: 'Stock Portfolio Tracker',
url: 'https://simpleportfolio.app'
@ -1071,7 +1071,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'simply-wallstreet',
name: 'Stock Portfolio Tracker & Visualizer by Simply Wall St',
origin: 'Australia',
origin: 'AU',
pricingPerYear: '$120',
slogan: 'Smart portfolio tracker for informed investors',
url: 'https://simplywall.st'
@ -1082,7 +1082,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'snowball-analytics',
name: 'Snowball Analytics',
origin: 'France',
origin: 'FR',
pricingPerYear: '$80',
slogan: 'Simple and powerful portfolio tracker',
url: 'https://snowball-analytics.com'
@ -1090,7 +1090,7 @@ export const personalFinanceTools: Product[] = [
{
key: 'splashmoney',
name: 'SplashMoney',
origin: 'United States',
origin: 'US',
slogan: 'Manage your money anytime, anywhere.'
},
{
@ -1098,14 +1098,14 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'stock-events',
name: 'Stock Events',
origin: 'Germany',
origin: 'DE',
slogan: 'Track all your Investments',
url: 'https://stockevents.app'
},
{
key: 'stockle',
name: 'Stockle',
origin: 'Finland',
origin: 'FI',
slogan: 'Supercharge your investments tracking experience',
url: 'https://stockle.app'
},
@ -1114,7 +1114,7 @@ export const personalFinanceTools: Product[] = [
isArchived: true,
key: 'stockmarketeye',
name: 'StockMarketEye',
origin: 'France',
origin: 'FR',
note: 'StockMarketEye was discontinued in 2023',
slogan: 'A Powerful Portfolio & Investment Tracking App'
},
@ -1124,7 +1124,7 @@ export const personalFinanceTools: Product[] = [
key: 'stock-rover',
languages: ['English'],
name: 'Stock Rover',
origin: 'United States',
origin: 'US',
pricingPerYear: '$79.99',
slogan: 'Investment Research and Portfolio Management',
url: 'https://www.stockrover.com'
@ -1135,7 +1135,7 @@ export const personalFinanceTools: Product[] = [
key: 'stonksfolio',
languages: ['English'],
name: 'Stonksfolio',
origin: 'Bulgaria',
origin: 'BG',
pricingPerYear: '€49.90',
slogan: 'Visualize all of your portfolios',
url: 'https://stonksfolio.com'
@ -1145,7 +1145,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'sumio',
name: 'Sumio',
origin: 'Czech Republic',
origin: 'CZ',
pricingPerYear: '$20',
slogan: 'Sum up and build your wealth.',
url: 'https://www.sumio.app'
@ -1155,7 +1155,7 @@ export const personalFinanceTools: Product[] = [
hasFreePlan: false,
key: 'tiller',
name: 'Tiller',
origin: 'United States',
origin: 'US',
pricingPerYear: '$79',
slogan:
'Your financial life in a spreadsheet, automatically updated each day',
@ -1167,7 +1167,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'tradervue',
name: 'Tradervue',
origin: 'United States',
origin: 'US',
pricingPerYear: '$360',
slogan: 'The Trading Journal to Improve Your Trading Performance',
url: 'https://www.tradervue.com'
@ -1199,7 +1199,7 @@ export const personalFinanceTools: Product[] = [
key: 'tresor-one',
name: 'Tresor One',
note: 'Renamed to Parqet',
origin: 'Germany',
origin: 'DE',
regions: ['Austria', 'Germany', 'Switzerland'],
slogan: 'Dein Vermögen immer im Blick'
},
@ -1209,7 +1209,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'turbobulls',
name: 'Turbobulls',
origin: 'Romania',
origin: 'RO',
pricingPerYear: '€39.99',
slogan: 'Your complete financial dashboard. Actually private.',
url: 'https://www.turbobulls.com'
@ -1220,7 +1220,7 @@ export const personalFinanceTools: Product[] = [
key: 'utluna',
languages: ['Deutsch', 'English', 'Français'],
name: 'Utluna',
origin: 'Switzerland',
origin: 'CH',
pricingPerYear: '$300',
slogan: 'Your Portfolio. Revealed.',
url: 'https://www.utluna.com',
@ -1231,7 +1231,7 @@ export const personalFinanceTools: Product[] = [
hasFreePlan: true,
key: 'vyzer',
name: 'Vyzer',
origin: 'United States',
origin: 'US',
pricingPerYear: '$348',
slogan: 'Virtual Family Office for Smart Wealth Management',
url: 'https://vyzer.co'
@ -1242,7 +1242,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'walletguide',
name: 'Walletguide',
origin: 'Germany',
origin: 'DE',
pricingPerYear: '€90',
slogan: 'Personal finance reimagined with AI',
url: 'https://walletguide.com'
@ -1254,7 +1254,7 @@ export const personalFinanceTools: Product[] = [
languages: ['English'],
name: 'wallmine',
note: 'wallmine was discontinued in 2024',
origin: 'Czech Republic',
origin: 'CZ',
pricingPerYear: '$600',
slogan: 'Make Smarter Investments'
},
@ -1264,7 +1264,7 @@ export const personalFinanceTools: Product[] = [
key: 'wealthbrain',
languages: ['English'],
name: 'Wealthbrain',
origin: 'United Arab Emirates',
origin: 'AE',
slogan: 'Portfolio Management System',
url: 'https://wealthbrain.com'
},
@ -1276,7 +1276,7 @@ export const personalFinanceTools: Product[] = [
key: 'wealthfolio',
languages: ['English'],
name: 'Wealthfolio',
origin: 'Canada',
origin: 'CA',
slogan: 'Desktop Investment Tracker',
url: 'https://wealthfolio.app'
},
@ -1287,7 +1287,7 @@ export const personalFinanceTools: Product[] = [
key: 'wealthica',
languages: ['English', 'Français'],
name: 'Wealthica',
origin: 'Canada',
origin: 'CA',
pricingPerYear: '$50',
slogan: 'See all your investments in one place',
url: 'https://wealthica.com'
@ -1307,14 +1307,14 @@ export const personalFinanceTools: Product[] = [
key: 'wealthy-tracker',
languages: ['English'],
name: 'Wealthy Tracker',
origin: 'India',
origin: 'IN',
slogan: 'One app to manage all your investments',
url: 'https://www.wealthy.in/tracker'
},
{
key: 'whal',
name: 'Whal',
origin: 'United States',
origin: 'US',
slogan: 'Manage your investments in one place'
},
{
@ -1326,7 +1326,7 @@ export const personalFinanceTools: Product[] = [
languages: ['Deutsch', 'English', 'Español', 'Français', 'Italiano'],
name: 'yeekatee',
note: 'yeekatee was discontinued in 2024',
origin: 'Switzerland',
origin: 'CH',
regions: ['Global'],
slogan: 'Connect. Share. Invest.'
},
@ -1336,7 +1336,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'ynab',
name: 'YNAB (You Need a Budget)',
origin: 'United States',
origin: 'US',
pricingPerYear: '$109',
slogan: 'Change Your Relationship With Money',
url: 'https://www.ynab.com'
@ -1347,7 +1347,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'ziggma',
name: 'Ziggma',
origin: 'United States',
origin: 'US',
pricingPerYear: '$84',
slogan: 'Your solution for investing success',
url: 'https://ziggma.com'

2
libs/common/src/lib/types/index.ts

@ -17,6 +17,7 @@ import type { MarketState } from './market-state.type';
import type { Market } from './market.type';
import type { OrderWithAccount } from './order-with-account.type';
import type { RequestWithUser } from './request-with-user.type';
import type { SectorName } from './sector-name.type';
import type { SubscriptionOfferKey } from './subscription-offer-key.type';
import type { UserWithSettings } from './user-with-settings.type';
import type { ViewMode } from './view-mode.type';
@ -41,6 +42,7 @@ export type {
MarketState,
OrderWithAccount,
RequestWithUser,
SectorName,
SubscriptionOfferKey,
UserWithSettings,
ViewMode

3
libs/common/src/lib/types/sector-name.type.ts

@ -0,0 +1,3 @@
import type { SECTORS } from '../config';
export type SectorName = (typeof SECTORS)[number];

13
libs/ui/src/lib/account-balances/account-balances.component.ts

@ -13,6 +13,7 @@ import {
OnChanges,
OnInit,
Output,
effect,
inject,
input,
viewChild
@ -70,6 +71,7 @@ export class GfAccountBalancesComponent implements OnChanges, OnInit {
input.required<AccountBalancesResponse['balances']>();
public readonly accountCurrency = input.required<string>();
public readonly accountId = input.required<string>();
public readonly currentBalance = input<number | null>();
public readonly displayedColumns: string[] = ['date', 'value', 'actions'];
public readonly locale = input(getLocale());
public readonly showActions = input(true);
@ -89,6 +91,17 @@ export class GfAccountBalancesComponent implements OnChanges, OnInit {
public constructor() {
addIcons({ calendarClearOutline, ellipsisHorizontal, trashOutline });
effect(() => {
const currentBalance = this.currentBalance();
if (
this.accountBalanceForm.controls.balance.pristine &&
typeof currentBalance === 'number'
) {
this.accountBalanceForm.controls.balance.setValue(currentBalance);
}
});
}
public ngOnInit() {

18
libs/ui/src/lib/fab/fab.component.scss

@ -1,14 +1,32 @@
:host {
display: block;
// Reserve space so a floating action button does not overlap trailing content
height: calc(constant(safe-area-inset-bottom) + 7rem);
height: calc(env(safe-area-inset-bottom) + 7rem);
@media (min-width: 576px) {
height: calc(constant(safe-area-inset-bottom) + 5rem);
height: calc(env(safe-area-inset-bottom) + 5rem);
}
.mat-mdc-fab {
bottom: calc(constant(safe-area-inset-bottom) + 2rem);
bottom: calc(env(safe-area-inset-bottom) + 2rem);
position: fixed;
right: 2rem;
z-index: 999;
}
}
:host-context(gf-page-tabs) {
@media (max-width: 575.98px) {
height: calc(constant(safe-area-inset-bottom) + 6rem);
height: calc(env(safe-area-inset-bottom) + 6rem);
.mat-mdc-fab {
bottom: calc(constant(safe-area-inset-bottom) + 5rem);
bottom: calc(env(safe-area-inset-bottom) + 5rem);
}
}
}

49
libs/ui/src/lib/i18n.ts

@ -1,3 +1,5 @@
import type { SectorName } from '@ghostfolio/common/types';
import '@angular/localize/init';
const locales = {
@ -73,42 +75,27 @@ const locales = {
Oceania: $localize`Oceania`,
'South America': $localize`South America`,
// Countries
Armenia: $localize`Armenia`,
Argentina: $localize`Argentina`,
Australia: $localize`Australia`,
Austria: $localize`Austria`,
Belgium: $localize`Belgium`,
'British Virgin Islands': $localize`British Virgin Islands`,
Bulgaria: $localize`Bulgaria`,
Canada: $localize`Canada`,
'Czech Republic': $localize`Czech Republic`,
Finland: $localize`Finland`,
France: $localize`France`,
Germany: $localize`Germany`,
India: $localize`India`,
Indonesia: $localize`Indonesia`,
Italy: $localize`Italy`,
Japan: $localize`Japan`,
Netherlands: $localize`Netherlands`,
'New Zealand': $localize`New Zealand`,
Poland: $localize`Poland`,
Romania: $localize`Romania`,
Singapore: $localize`Singapore`,
'South Africa': $localize`South Africa`,
Switzerland: $localize`Switzerland`,
Thailand: $localize`Thailand`,
Ukraine: $localize`Ukraine`,
'United Kingdom': $localize`United Kingdom`,
'United States': $localize`United States`,
// Fear and Greed Index
EXTREME_FEAR: $localize`Extreme Fear`,
EXTREME_GREED: $localize`Extreme Greed`,
FEAR: $localize`Fear`,
GREED: $localize`Greed`,
NEUTRAL: $localize`Neutral`
};
NEUTRAL: $localize`Neutral`,
// Sectors
'Basic Materials': $localize`Basic Materials`,
'Communication Services': $localize`Communication Services`,
'Consumer Cyclical': $localize`Consumer Cyclical`,
'Consumer Defensive': $localize`Consumer Defensive`,
Energy: $localize`Energy`,
'Financial Services': $localize`Financial Services`,
Healthcare: $localize`Healthcare`,
Industrials: $localize`Industrials`,
Other: $localize`Other`,
'Real Estate': $localize`Real Estate`,
Technology: $localize`Technology`,
Utilities: $localize`Utilities`
} satisfies Record<SectorName, string> & Record<string, string>;
export function translate(aKey: string): string {
return locales[aKey] ?? aKey;

6
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -37,8 +37,6 @@ import Color from 'color';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color';
import { translate } from '../i18n';
const {
blue,
cyan,
@ -390,7 +388,7 @@ export class GfPortfolioProportionChartComponent
return value > 0
? isUUID(symbol)
? (translate(this.data[symbol]?.name) ?? symbol)
? (this.data[symbol]?.name ?? symbol)
: symbol
: '';
},
@ -453,7 +451,7 @@ export class GfPortfolioProportionChartComponent
symbol = $localize`No data available`;
}
const name = translate(this.data[symbol]?.name);
const name = this.data[symbol]?.name;
let sum = 0;

16
libs/ui/src/lib/world-map-chart/world-map-chart.component.ts

@ -1,4 +1,8 @@
import { getLocale, getNumberFormatGroup } from '@ghostfolio/common/helper';
import {
getCountryName,
getLocale,
getNumberFormatGroup
} from '@ghostfolio/common/helper';
import {
ChangeDetectionStrategy,
@ -25,7 +29,7 @@ export class GfWorldMapChartComponent implements OnChanges, OnDestroy {
@Input() locale = getLocale();
public isLoading = true;
public svgMapElement;
public svgMapElement: any;
public constructor(private changeDetectorRef: ChangeDetectorRef) {}
@ -88,6 +92,14 @@ export class GfWorldMapChartComponent implements OnChanges, OnDestroy {
targetElementID: 'svgMap'
});
this.svgMapElement.options.countryNames = Object.keys(
this.svgMapElement.countries
).reduce<{ [code: string]: string }>((names, code) => {
names[code] = getCountryName({ code, locale: this.locale });
return names;
}, {});
setTimeout(() => {
this.isLoading = false;

4
package-lock.json

@ -1,12 +1,12 @@
{
"name": "ghostfolio",
"version": "3.7.0",
"version": "3.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "3.7.0",
"version": "3.8.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

2
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "3.7.0",
"version": "3.8.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",

Loading…
Cancel
Save