Compare commits

...

4 Commits

Author SHA1 Message Date
github-actions[bot] b3f20e175c
Task/update locales (#6902) 3 days ago
Thomas Kaul 363684526f
Task/localize country names (#6995) 3 days ago
Thomas Kaul 3a4b0ce304
Task/harmonize sector names accross data providers (#6994) 3 days ago
Thomas Kaul bf7409ec20
Task/refactor country code logic in data provider services (#6993) 3 days ago
  1. 3
      CHANGELOG.md
  2. 17
      apps/api/src/helper/country.helper.ts
  3. 28
      apps/api/src/helper/sector.helper.ts
  4. 39
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  5. 66
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  6. 24
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  7. 10
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  8. 9
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  9. 15
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  10. 9
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  11. 13
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  12. 11
      apps/client/src/app/pages/public/public-page.component.ts
  13. 770
      apps/client/src/locales/messages.ca.xlf
  14. 770
      apps/client/src/locales/messages.de.xlf
  15. 770
      apps/client/src/locales/messages.es.xlf
  16. 770
      apps/client/src/locales/messages.fr.xlf
  17. 770
      apps/client/src/locales/messages.it.xlf
  18. 770
      apps/client/src/locales/messages.ko.xlf
  19. 770
      apps/client/src/locales/messages.nl.xlf
  20. 770
      apps/client/src/locales/messages.pl.xlf
  21. 770
      apps/client/src/locales/messages.pt.xlf
  22. 770
      apps/client/src/locales/messages.tr.xlf
  23. 770
      apps/client/src/locales/messages.uk.xlf
  24. 734
      apps/client/src/locales/messages.xlf
  25. 770
      apps/client/src/locales/messages.zh.xlf
  26. 15
      libs/common/src/lib/config.ts
  27. 14
      libs/common/src/lib/helper.ts
  28. 2
      libs/common/src/lib/types/index.ts
  29. 3
      libs/common/src/lib/types/sector-name.type.ts
  30. 49
      libs/ui/src/lib/i18n.ts
  31. 6
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  32. 16
      libs/ui/src/lib/world-map-chart/world-map-chart.component.ts

3
CHANGELOG.md

@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### 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 - 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 - 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 - Prevented the deletion of asset profiles that are currently in use

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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service'; import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { Holding } from '@ghostfolio/common/interfaces'; import { Holding } from '@ghostfolio/common/interfaces';
import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { SectorName } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import { countries } from 'countries-list';
@Injectable() @Injectable()
export class TrackinsightDataEnhancerService implements DataEnhancerInterface { export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
private readonly logger = new Logger(TrackinsightDataEnhancerService.name);
private static baseUrl = 'https://www.trackinsight.com/data-api'; private static baseUrl = 'https://www.trackinsight.com/data-api';
private static countriesMapping = { private static countriesMapping = {
'Russian Federation': 'Russia', 'Russian Federation': 'Russia',
USA: 'United States' USA: 'United States'
}; };
private static holdingsWeightTreshold = 0.85; private static holdingsWeightTreshold = 0.85;
private static sectorsMapping = { private static sectorsMapping: Record<string, SectorName> = {
'Consumer Discretionary': 'Consumer Cyclical', 'Consumer Discretionary': 'Consumer Cyclical',
'Consumer Defensive': 'Consumer Staples', 'Consumer Staples': 'Consumer Defensive',
Financials: 'Financial Services',
'Health Care': 'Healthcare', 'Health Care': 'Healthcare',
'Information Technology': 'Technology' 'Information Technology': 'Technology',
Materials: 'Basic Materials'
}; };
private readonly logger = new Logger(TrackinsightDataEnhancerService.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService private readonly fetchService: FetchService
@ -117,21 +121,11 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
for (const [name, value] of Object.entries<any>( for (const [name, value] of Object.entries<any>(
holdings?.countries ?? {} 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({ response.countries.push({
code: countryCode, code: getCountryCodeByName({
name,
aliases: TrackinsightDataEnhancerService.countriesMapping
}),
weight: value.weight weight: value.weight
}); });
} }
@ -165,7 +159,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
holdings?.sectors ?? {} holdings?.sectors ?? {}
)) { )) {
response.sectors.push({ response.sectors.push({
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name, name: getSectorName({
name,
aliases: TrackinsightDataEnhancerService.sectorsMapping
}),
weight: value.weight 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 { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error'; 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 { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
REPLACE_NAME_PARTS, REPLACE_NAME_PARTS
UNKNOWN_KEY
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { isCurrency } from '@ghostfolio/common/helper'; import { isCurrency } from '@ghostfolio/common/helper';
import { SectorName } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
@ -23,6 +24,20 @@ import type { Price } from 'yahoo-finance2/esm/src/modules/quoteSummary-iface';
@Injectable() @Injectable()
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { 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 logger = new Logger(YahooFinanceDataEnhancerService.name);
private readonly yahooFinance = new YahooFinance({ private readonly yahooFinance = new YahooFinance({
@ -224,7 +239,10 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
.flatMap((sectorWeighting) => { .flatMap((sectorWeighting) => {
return Object.entries(sectorWeighting).map(([sector, weight]) => { return Object.entries(sectorWeighting).map(([sector, weight]) => {
return { return {
name: this.parseSector(sector), name: getSectorName({
aliases: YahooFinanceDataEnhancerService.sectorsMapping,
name: sector
}),
weight: weight as number weight: weight as number
}; };
}); });
@ -331,46 +349,4 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
return { assetClass, assetSubClass }; 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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error'; import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error';
@ -33,7 +34,6 @@ import {
SymbolProfile SymbolProfile
} from '@prisma/client'; } from '@prisma/client';
import { isISIN } from 'class-validator'; import { isISIN } from 'class-validator';
import { countries } from 'countries-list';
import { import {
addDays, addDays,
addYears, addYears,
@ -49,14 +49,14 @@ import { uniqBy } from 'lodash';
export class FinancialModelingPrepService export class FinancialModelingPrepService
implements DataProviderInterface, OnModuleInit implements DataProviderInterface, OnModuleInit
{ {
private readonly logger = new Logger(FinancialModelingPrepService.name);
private static countriesMapping = { private static countriesMapping = {
'Korea (the Republic of)': 'South Korea', 'Korea (the Republic of)': 'South Korea',
'Russian Federation': 'Russia', 'Russian Federation': 'Russia',
'Taiwan (Province of China)': 'Taiwan' 'Taiwan (Province of China)': 'Taiwan'
}; };
private readonly logger = new Logger(FinancialModelingPrepService.name);
private apiKey: string; private apiKey: string;
public constructor( public constructor(
@ -165,21 +165,11 @@ export class FinancialModelingPrepService
return countryName.toLowerCase() !== 'other'; return countryName.toLowerCase() !== 'other';
}) })
.map(({ country: countryName, weightPercentage }) => { .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 { return {
code: countryCode, code: getCountryCodeByName({
aliases: FinancialModelingPrepService.countriesMapping,
name: countryName
}),
weight: parseFloat(weightPercentage.slice(0, -1)) / 100 weight: parseFloat(weightPercentage.slice(0, -1)) / 100
}; };
}); });

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

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

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

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

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

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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config'; import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper'; import { getCountryName, prettifySymbol } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
HoldingWithParents, HoldingWithParents,
@ -353,7 +353,7 @@ export class GfAllocationsPageComponent implements OnInit {
if (position.assetProfile.countries.length > 0) { if (position.assetProfile.countries.length > 0) {
for (const country of position.assetProfile.countries) { for (const country of position.assetProfile.countries) {
const { code, continent, name, weight } = country; const { code, continent, weight } = country;
if (this.continents[continent]?.value) { if (this.continents[continent]?.value) {
this.continents[continent].value += this.continents[continent].value +=
@ -363,7 +363,7 @@ export class GfAllocationsPageComponent implements OnInit {
: position.valueInPercentage); : position.valueInPercentage);
} else { } else {
this.continents[continent] = { this.continents[continent] = {
name: continent, name: translate(continent),
value: value:
weight * weight *
(isNumber(position.valueInBaseCurrency) (isNumber(position.valueInBaseCurrency)
@ -380,7 +380,10 @@ export class GfAllocationsPageComponent implements OnInit {
: position.valueInPercentage); : position.valueInPercentage);
} else { } else {
this.countries[code] = { this.countries[code] = {
name, name: getCountryName({
code,
locale: this.user?.settings?.locale
}),
value: value:
weight * weight *
(isNumber(position.valueInBaseCurrency) (isNumber(position.valueInBaseCurrency)
@ -442,7 +445,7 @@ export class GfAllocationsPageComponent implements OnInit {
: position.valueInPercentage); : position.valueInPercentage);
} else { } else {
this.sectors[name] = { this.sectors[name] = {
name, name: translate(name),
value: value:
weight * weight *
(isNumber(position.valueInBaseCurrency) (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 { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper'; import { getCountryName, prettifySymbol } from '@ghostfolio/common/helper';
import { import {
InfoItem, InfoItem,
PortfolioPosition, PortfolioPosition,
@ -9,6 +9,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Market } from '@ghostfolio/common/types'; import { Market } from '@ghostfolio/common/types';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table/activities-table.component'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table/activities-table.component';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table/holdings-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 { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.component';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -185,14 +186,14 @@ export class GfPublicPageComponent implements OnInit {
if (position.assetProfile.countries.length > 0) { if (position.assetProfile.countries.length > 0) {
for (const country of position.assetProfile.countries) { for (const country of position.assetProfile.countries) {
const { code, continent, name, weight } = country; const { code, continent, weight } = country;
if (this.continents[continent]?.value) { if (this.continents[continent]?.value) {
this.continents[continent].value += this.continents[continent].value +=
weight * (position.valueInBaseCurrency ?? 0); weight * (position.valueInBaseCurrency ?? 0);
} else { } else {
this.continents[continent] = { this.continents[continent] = {
name: continent, name: translate(continent),
value: value:
weight * weight *
(this.publicPortfolioDetails.holdings[symbol] (this.publicPortfolioDetails.holdings[symbol]
@ -205,7 +206,7 @@ export class GfPublicPageComponent implements OnInit {
weight * (position.valueInBaseCurrency ?? 0); weight * (position.valueInBaseCurrency ?? 0);
} else { } else {
this.countries[code] = { this.countries[code] = {
name, name: getCountryName({ code }),
value: value:
weight * weight *
(this.publicPortfolioDetails.holdings[symbol] (this.publicPortfolioDetails.holdings[symbol]
@ -232,7 +233,7 @@ export class GfPublicPageComponent implements OnInit {
weight * (position.valueInBaseCurrency ?? 0); weight * (position.valueInBaseCurrency ?? 0);
} else { } else {
this.sectors[name] = { this.sectors[name] = {
name, name: translate(name),
value: value:
weight * weight *
(this.publicPortfolioDetails.holdings[symbol] (this.publicPortfolioDetails.holdings[symbol]

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 -' '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 STORYBOOK_PATH = '/development/storybook';
export const SUPPORTED_LANGUAGE_CODES = [ 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, ''); return aSymbol.replace(DEFAULT_CURRENCY, '');
} }
export function getCountryName({
code,
locale = getLocale()
}: {
code: string;
locale?: string;
}) {
try {
return new Intl.DisplayNames([locale], { type: 'region' }).of(code) ?? code;
} catch {
return code;
}
}
export function getDateFnsLocale(aLanguageCode?: string) { export function getDateFnsLocale(aLanguageCode?: string) {
if (aLanguageCode === 'ca') { if (aLanguageCode === 'ca') {
return ca; return ca;

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 { Market } from './market.type';
import type { OrderWithAccount } from './order-with-account.type'; import type { OrderWithAccount } from './order-with-account.type';
import type { RequestWithUser } from './request-with-user.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 { SubscriptionOfferKey } from './subscription-offer-key.type';
import type { UserWithSettings } from './user-with-settings.type'; import type { UserWithSettings } from './user-with-settings.type';
import type { ViewMode } from './view-mode.type'; import type { ViewMode } from './view-mode.type';
@ -41,6 +42,7 @@ export type {
MarketState, MarketState,
OrderWithAccount, OrderWithAccount,
RequestWithUser, RequestWithUser,
SectorName,
SubscriptionOfferKey, SubscriptionOfferKey,
UserWithSettings, UserWithSettings,
ViewMode 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];

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

@ -1,3 +1,5 @@
import type { SectorName } from '@ghostfolio/common/types';
import '@angular/localize/init'; import '@angular/localize/init';
const locales = { const locales = {
@ -73,42 +75,27 @@ const locales = {
Oceania: $localize`Oceania`, Oceania: $localize`Oceania`,
'South America': $localize`South America`, '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 // Fear and Greed Index
EXTREME_FEAR: $localize`Extreme Fear`, EXTREME_FEAR: $localize`Extreme Fear`,
EXTREME_GREED: $localize`Extreme Greed`, EXTREME_GREED: $localize`Extreme Greed`,
FEAR: $localize`Fear`, FEAR: $localize`Fear`,
GREED: $localize`Greed`, 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 { export function translate(aKey: string): string {
return locales[aKey] ?? aKey; 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 { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color'; import OpenColor from 'open-color';
import { translate } from '../i18n';
const { const {
blue, blue,
cyan, cyan,
@ -390,7 +388,7 @@ export class GfPortfolioProportionChartComponent
return value > 0 return value > 0
? isUUID(symbol) ? isUUID(symbol)
? (translate(this.data[symbol]?.name) ?? symbol) ? (this.data[symbol]?.name ?? symbol)
: symbol : symbol
: ''; : '';
}, },
@ -453,7 +451,7 @@ export class GfPortfolioProportionChartComponent
symbol = $localize`No data available`; symbol = $localize`No data available`;
} }
const name = translate(this.data[symbol]?.name); const name = this.data[symbol]?.name;
let sum = 0; 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 { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
@ -25,7 +29,7 @@ export class GfWorldMapChartComponent implements OnChanges, OnDestroy {
@Input() locale = getLocale(); @Input() locale = getLocale();
public isLoading = true; public isLoading = true;
public svgMapElement; public svgMapElement: any;
public constructor(private changeDetectorRef: ChangeDetectorRef) {} public constructor(private changeDetectorRef: ChangeDetectorRef) {}
@ -88,6 +92,14 @@ export class GfWorldMapChartComponent implements OnChanges, OnDestroy {
targetElementID: 'svgMap' 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(() => { setTimeout(() => {
this.isLoading = false; this.isLoading = false;

Loading…
Cancel
Save