Browse Source

Merge branch 'main' into task/centralize-asset-profile-override-logic

pull/6991/head
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
f6fd9e33f9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 4
      apps/api/src/app/admin/admin.controller.ts
  3. 4
      apps/api/src/app/auth/auth.module.ts
  4. 4
      apps/api/src/app/auth/google.strategy.ts
  5. 9
      apps/api/src/app/auth/oidc.strategy.ts
  6. 6
      apps/api/src/app/auth/web-auth.service.ts
  7. 7
      apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts
  8. 12
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  9. 4
      apps/api/src/app/health/health.controller.ts
  10. 4
      apps/api/src/app/import/import.controller.ts
  11. 7
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  12. 6
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  13. 7
      apps/api/src/app/portfolio/portfolio.service.ts
  14. 6
      apps/api/src/app/redis-cache/redis-cache.service.ts
  15. 14
      apps/api/src/app/subscription/subscription.controller.ts
  16. 9
      apps/api/src/app/subscription/subscription.service.ts
  17. 4
      apps/api/src/app/symbol/symbol.service.ts
  18. 12
      apps/api/src/events/asset-profile-changed.listener.ts
  19. 7
      apps/api/src/events/portfolio-changed.listener.ts
  20. 4
      apps/api/src/interceptors/performance-logging/performance-logging.service.ts
  21. 18
      apps/api/src/main.ts
  22. 8
      apps/api/src/middlewares/html-template.middleware.ts
  23. 6
      apps/api/src/services/benchmark/benchmark.service.ts
  24. 8
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  25. 7
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  26. 6
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  27. 31
      apps/api/src/services/data-provider/data-provider.service.ts
  28. 21
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  29. 13
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  30. 12
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  31. 4
      apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
  32. 9
      apps/api/src/services/data-provider/manual/manual.service.ts
  33. 6
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  34. 23
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  35. 19
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  36. 24
      apps/api/src/services/fetch/fetch.service.ts
  37. 8
      apps/api/src/services/i18n/i18n.service.ts
  38. 4
      apps/api/src/services/prisma/prisma.service.ts
  39. 42
      apps/api/src/services/queues/data-gathering/data-gathering.processor.ts
  40. 15
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts
  41. 17
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts
  42. 55
      apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts
  43. 9
      apps/api/src/services/twitter-bot/twitter-bot.service.ts
  44. 94
      apps/client/src/locales/messages.uk.xlf

2
CHANGELOG.md

@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Centralized the asset profile override logic for manual adjustments
- Refactored the backend logging to use the instance-based `Logger`
- Improved the language localization for Ukrainian (`uk`)
### Fixed

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

@ -58,6 +58,8 @@ import { AdminService } from './admin.service';
@Controller('admin')
export class AdminController {
private readonly logger = new Logger(AdminController.name);
public constructor(
private readonly adminService: AdminService,
private readonly apiService: ApiService,
@ -260,7 +262,7 @@ export class AdminController {
`Could not parse the market price for ${symbol} (${dataSource})`
);
} catch (error) {
Logger.error(error, 'AdminController');
this.logger.error(error);
throw new HttpException(error.message, StatusCodes.BAD_REQUEST);
}

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

@ -50,6 +50,8 @@ import { OidcStrategy } from './oidc.strategy';
configurationService: ConfigurationService,
fetchService: FetchService
) => {
const logger = new Logger('OidcStrategy');
const isOidcEnabled = configurationService.get(
'ENABLE_FEATURE_AUTH_OIDC'
);
@ -101,7 +103,7 @@ import { OidcStrategy } from './oidc.strategy';
tokenURL = manualTokenUrl || config.token_endpoint;
userInfoURL = manualUserInfoUrl || config.userinfo_endpoint;
} catch (error) {
Logger.error(error, 'OidcStrategy');
logger.error(error);
throw new Error('Failed to fetch OIDC configuration from issuer');
}
}

4
apps/api/src/app/auth/google.strategy.ts

@ -10,6 +10,8 @@ import { AuthService } from './auth.service';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
private readonly logger = new Logger(GoogleStrategy.name);
public constructor(
private readonly authService: AuthService,
configurationService: ConfigurationService
@ -40,7 +42,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
done(null, { jwt });
} catch (error) {
Logger.error(error, 'GoogleStrategy');
this.logger.error(error);
done(error, false);
}
}

9
apps/api/src/app/auth/oidc.strategy.ts

@ -15,6 +15,8 @@ import { OidcStateStore } from './oidc-state.store';
@Injectable()
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
private readonly logger = new Logger(OidcStrategy.name);
private static readonly stateStore = new OidcStateStore();
public constructor(
@ -52,9 +54,8 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
});
if (!thirdPartyId) {
Logger.error(
`Missing subject identifier in OIDC response from ${issuer}`,
'OidcStrategy'
this.logger.error(
`Missing subject identifier in OIDC response from ${issuer}`
);
throw new Error('Missing subject identifier in OIDC response');
@ -62,7 +63,7 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
return { jwt };
} catch (error) {
Logger.error(error, 'OidcStrategy');
this.logger.error(error);
throw error;
}
}

6
apps/api/src/app/auth/web-auth.service.ts

@ -33,6 +33,8 @@ import ms from 'ms';
@Injectable()
export class WebAuthService {
private readonly logger = new Logger(WebAuthService.name);
public constructor(
private readonly configurationService: ConfigurationService,
private readonly deviceService: AuthDeviceService,
@ -103,7 +105,7 @@ export class WebAuthService {
verification = await verifyRegistrationResponse(opts);
} catch (error) {
Logger.error(error, 'WebAuthService');
this.logger.error(error);
throw new InternalServerErrorException(error.message);
}
@ -210,7 +212,7 @@ export class WebAuthService {
verification = await verifyAuthenticationResponse(opts);
} catch (error) {
Logger.error(error, 'WebAuthService');
this.logger.error(error);
throw new InternalServerErrorException({ error: error.message });
}

7
apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts

@ -17,6 +17,8 @@ import { isNumber } from 'lodash';
@Injectable()
export class BenchmarksService {
private readonly logger = new Logger(BenchmarksService.name);
public constructor(
private readonly benchmarkService: BenchmarkService,
private readonly exchangeRateDataService: ExchangeRateDataService,
@ -96,12 +98,11 @@ export class BenchmarksService {
})?.marketPrice;
if (!marketPriceAtStartDate) {
Logger.error(
this.logger.error(
`No historical market data has been found for ${symbol} (${dataSource}) at ${format(
startDate,
DATE_FORMAT
)}`,
'BenchmarkService'
)}`
);
return { marketData };

12
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -34,6 +34,8 @@ import { Big } from 'big.js';
@Injectable()
export class GhostfolioService {
private readonly logger = new Logger(GhostfolioService.name);
public constructor(
private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService,
@ -99,7 +101,7 @@ export class GhostfolioService {
return result;
} catch (error) {
Logger.error(error, 'GhostfolioService');
this.logger.error(error);
throw error;
}
@ -141,7 +143,7 @@ export class GhostfolioService {
return result;
} catch (error) {
Logger.error(error, 'GhostfolioService');
this.logger.error(error);
throw error;
}
@ -183,7 +185,7 @@ export class GhostfolioService {
return result;
} catch (error) {
Logger.error(error, 'GhostfolioService');
this.logger.error(error);
throw error;
}
@ -271,7 +273,7 @@ export class GhostfolioService {
return results;
} catch (error) {
Logger.error(error, 'GhostfolioService');
this.logger.error(error);
throw error;
}
@ -348,7 +350,7 @@ export class GhostfolioService {
return results;
} catch (error) {
Logger.error(error, 'GhostfolioService');
this.logger.error(error);
throw error;
}

4
apps/api/src/app/health/health.controller.ts

@ -24,6 +24,8 @@ import { HealthService } from './health.service';
@Controller('health')
export class HealthController {
private readonly logger = new Logger(HealthController.name);
public constructor(
private readonly aiService: AiService,
private readonly healthService: HealthService
@ -61,7 +63,7 @@ export class HealthController {
.json({ status: getReasonPhrase(StatusCodes.OK) });
}
} catch (error) {
Logger.error(error, 'HealthController');
this.logger.error(error);
}
return response

4
apps/api/src/app/import/import.controller.ts

@ -31,6 +31,8 @@ import { ImportService } from './import.service';
@Controller('import')
export class ImportController {
private readonly logger = new Logger(ImportController.name);
public constructor(
private readonly configurationService: ConfigurationService,
private readonly importService: ImportService,
@ -81,7 +83,7 @@ export class ImportController {
return { activities };
} catch (error) {
Logger.error(error, ImportController);
this.logger.error(error);
throw new HttpException(
{

7
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -62,6 +62,8 @@ import { isNumber, sortBy, sum, uniqBy } from 'lodash';
export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false;
protected readonly logger = new Logger(PortfolioCalculator.name);
protected accountBalanceItems: HistoricalDataItem[];
protected activities: PortfolioOrder[];
@ -1119,12 +1121,11 @@ export abstract class PortfolioCalculator {
if (cachedPortfolioSnapshot) {
this.snapshot = cachedPortfolioSnapshot;
Logger.debug(
this.logger.debug(
`Fetched portfolio snapshot from cache in ${(
(performance.now() - startTimeTotal) /
1000
).toFixed(3)} seconds`,
'PortfolioCalculator'
).toFixed(3)} seconds`
);
if (isCachedPortfolioSnapshotExpired) {

6
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

@ -11,7 +11,6 @@ import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
import { DateRange } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Logger } from '@nestjs/common';
import { Big } from 'big.js';
import {
addMilliseconds,
@ -96,9 +95,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
currentPosition.timeWeightedInvestmentWithCurrencyEffect
);
} else if (!currentPosition.quantity.eq(0)) {
Logger.warn(
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`,
'PortfolioCalculator'
this.logger.warn(
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`
);
hasErrors = true;

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

@ -108,6 +108,8 @@ const europeMarkets = require('../../assets/countries/europe-markets.json');
@Injectable()
export class PortfolioService {
private readonly logger = new Logger(PortfolioService.name);
public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly accountService: AccountService,
@ -619,9 +621,8 @@ export class PortfolioService {
symbolProfileMap[getAssetProfileIdentifier({ dataSource, symbol })];
if (!assetProfile) {
Logger.warn(
`Asset profile not found for ${symbol} (${dataSource})`,
'PortfolioService'
this.logger.warn(
`Asset profile not found for ${symbol} (${dataSource})`
);
continue;

6
apps/api/src/app/redis-cache/redis-cache.service.ts

@ -10,6 +10,8 @@ import { createHash, randomUUID } from 'node:crypto';
@Injectable()
export class RedisCacheService {
private readonly logger = new Logger(RedisCacheService.name);
private client: Keyv;
public constructor(
@ -27,7 +29,7 @@ export class RedisCacheService {
};
this.client.on('error', (error) => {
Logger.error(error, 'RedisCacheService');
this.logger.error(error);
});
}
@ -101,7 +103,7 @@ export class RedisCacheService {
return true;
} catch (error) {
Logger.error(error?.message, 'RedisCacheService');
this.logger.error(error?.message);
return false;
} finally {

14
apps/api/src/app/subscription/subscription.controller.ts

@ -33,6 +33,8 @@ import { SubscriptionService } from './subscription.service';
@Controller('subscription')
export class SubscriptionController {
private readonly logger = new Logger(SubscriptionController.name);
public constructor(
private readonly configurationService: ConfigurationService,
private readonly propertyService: PropertyService,
@ -80,9 +82,8 @@ export class SubscriptionController {
value: JSON.stringify(coupons)
});
Logger.log(
`Subscription for user '${this.request.user.id}' has been created with a coupon for ${coupon.duration}`,
'SubscriptionController'
this.logger.log(
`Subscription for user '${this.request.user.id}' has been created with a coupon for ${coupon.duration}`
);
return {
@ -101,9 +102,8 @@ export class SubscriptionController {
);
if (userId) {
Logger.log(
`Subscription for user '${userId}' has been created via Stripe`,
'SubscriptionController'
this.logger.log(
`Subscription for user '${userId}' has been created via Stripe`
);
}
@ -126,7 +126,7 @@ export class SubscriptionController {
user: this.request.user
});
} catch (error) {
Logger.error(error, 'SubscriptionController');
this.logger.error(error);
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),

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

@ -24,6 +24,8 @@ import Stripe from 'stripe';
@Injectable()
export class SubscriptionService {
private readonly logger = new Logger(SubscriptionService.name);
private stripe: Stripe;
public constructor(
@ -166,9 +168,8 @@ export class SubscriptionService {
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
Logger.log(
`Stripe Checkout Session '${session.id}' has already been redeemed`,
'SubscriptionService'
this.logger.log(
`Stripe Checkout Session '${session.id}' has already been redeemed`
);
} else {
throw error;
@ -177,7 +178,7 @@ export class SubscriptionService {
return session.client_reference_id;
} catch (error) {
Logger.error(error, 'SubscriptionService');
this.logger.error(error);
}
}

4
apps/api/src/app/symbol/symbol.service.ts

@ -15,6 +15,8 @@ import { format, subDays } from 'date-fns';
@Injectable()
export class SymbolService {
private readonly logger = new Logger(SymbolService.name);
public constructor(
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService
@ -119,7 +121,7 @@ export class SymbolService {
results.items = items;
return results;
} catch (error) {
Logger.error(error, 'SymbolService');
this.logger.error(error);
throw error;
}

12
apps/api/src/events/asset-profile-changed.listener.ts

@ -15,6 +15,8 @@ import { AssetProfileChangedEvent } from './asset-profile-changed.event';
@Injectable()
export class AssetProfileChangedListener {
private readonly logger = new Logger(AssetProfileChangedListener.name);
private static readonly DEBOUNCE_DELAY = ms('5 seconds');
private debounceTimers = new Map<string, NodeJS.Timeout>();
@ -67,10 +69,7 @@ export class AssetProfileChangedListener {
dataSource: DataSource;
symbol: string;
}) {
Logger.log(
`Asset profile of ${symbol} (${dataSource}) has changed`,
'AssetProfileChangedListener'
);
this.logger.log(`Asset profile of ${symbol} (${dataSource}) has changed`);
if (
this.configurationService.get(
@ -84,10 +83,7 @@ export class AssetProfileChangedListener {
const existingCurrencies = this.exchangeRateDataService.getCurrencies();
if (!existingCurrencies.includes(currency)) {
Logger.log(
`New currency ${currency} has been detected`,
'AssetProfileChangedListener'
);
this.logger.log(`New currency ${currency} has been detected`);
await this.exchangeRateDataService.initialize();
}

7
apps/api/src/events/portfolio-changed.listener.ts

@ -8,6 +8,8 @@ import { PortfolioChangedEvent } from './portfolio-changed.event';
@Injectable()
export class PortfolioChangedListener {
private readonly logger = new Logger(PortfolioChangedListener.name);
private static readonly DEBOUNCE_DELAY = ms('5 seconds');
private debounceTimers = new Map<string, NodeJS.Timeout>();
@ -35,10 +37,7 @@ export class PortfolioChangedListener {
}
private async processPortfolioChanged({ userId }: { userId: string }) {
Logger.log(
`Portfolio of user '${userId}' has changed`,
'PortfolioChangedListener'
);
this.logger.log(`Portfolio of user '${userId}' has changed`);
await this.redisCacheService.removePortfolioSnapshotsByUserId({ userId });
}

4
apps/api/src/interceptors/performance-logging/performance-logging.service.ts

@ -2,6 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class PerformanceLoggingService {
private readonly logger = new Logger(PerformanceLoggingService.name);
public logPerformance({
className,
methodName,
@ -13,7 +15,7 @@ export class PerformanceLoggingService {
}) {
const endTime = performance.now();
Logger.debug(
this.logger.debug(
`Completed execution of ${methodName}() in ${((endTime - startTime) / 1000).toFixed(3)} seconds`,
className
);

18
apps/api/src/main.ts

@ -23,6 +23,8 @@ import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
const logger = new Logger('Bootstrap');
async function bootstrap() {
// Respect HTTP_PROXY / HTTPS_PROXY / NO_PROXY for outbound HTTP requests
setGlobalDispatcher(new EnvHttpProxyAgent());
@ -114,20 +116,20 @@ async function bootstrap() {
address = `${host}:${addressObject.port}`;
}
Logger.log(`Listening at http://${address}`);
Logger.log('');
logger.log(`Listening at http://${address}`);
logger.log('');
});
}
function logLogo() {
Logger.log(' ________ __ ____ ___');
Logger.log(' / ____/ /_ ____ _____/ /_/ __/___ / (_)___');
Logger.log(' / / __/ __ \\/ __ \\/ ___/ __/ /_/ __ \\/ / / __ \\');
Logger.log('/ /_/ / / / / /_/ (__ ) /_/ __/ /_/ / / / /_/ /');
Logger.log(
logger.log(' ________ __ ____ ___');
logger.log(' / ____/ /_ ____ _____/ /_/ __/___ / (_)___');
logger.log(' / / __/ __ \\/ __ \\/ ___/ __/ /_/ __ \\/ / / __ \\');
logger.log('/ /_/ / / / / /_/ (__ ) /_/ __/ /_/ / / / /_/ /');
logger.log(
`\\____/_/ /_/\\____/____/\\__/_/ \\____/_/_/\\____/ ${environment.version}`
);
Logger.log('');
logger.log('');
}
bootstrap();

8
apps/api/src/middlewares/html-template.middleware.ts

@ -92,6 +92,8 @@ const locales = {
@Injectable()
export class HtmlTemplateMiddleware implements NestMiddleware {
private readonly logger = new Logger(HtmlTemplateMiddleware.name);
private indexHtmlMap: { [languageCode: string]: string } = {};
public constructor(private readonly i18nService: I18nService) {
@ -107,11 +109,7 @@ export class HtmlTemplateMiddleware implements NestMiddleware {
{}
);
} catch (error) {
Logger.error(
'Failed to initialize index HTML map',
error,
'HTMLTemplateMiddleware'
);
this.logger.error('Failed to initialize index HTML map', error);
}
}

6
apps/api/src/services/benchmark/benchmark.service.ts

@ -28,6 +28,8 @@ import { BenchmarkValue } from './interfaces/benchmark-value.interface';
@Injectable()
export class BenchmarkService {
private readonly logger = new Logger(BenchmarkService.name);
private readonly CACHE_KEY_BENCHMARKS = 'BENCHMARKS';
public constructor(
@ -87,7 +89,7 @@ export class BenchmarkService {
const { benchmarks, expiration }: BenchmarkValue =
JSON.parse(cachedBenchmarkValue);
Logger.debug('Fetched benchmarks from cache', 'BenchmarkService');
this.logger.debug('Fetched benchmarks from cache');
if (isAfter(new Date(), new Date(expiration))) {
this.calculateAndCacheBenchmarks({
@ -227,7 +229,7 @@ export class BenchmarkService {
private async calculateAndCacheBenchmarks({
enableSharing = false
}): Promise<BenchmarkResponse['benchmarks']> {
Logger.debug('Calculate benchmarks', 'BenchmarkService');
this.logger.debug('Calculate benchmarks');
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles({
enableSharing

8
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -29,6 +29,8 @@ import { format, fromUnixTime, getUnixTime } from 'date-fns';
@Injectable()
export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
private readonly logger = new Logger(CoinGeckoService.name);
private apiUrl: string;
private headers: HeadersInit = {};
@ -88,7 +90,7 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
).toFixed(3)} seconds`;
}
Logger.error(message, 'CoinGeckoService');
this.logger.error(message);
}
return response;
@ -214,7 +216,7 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
).toFixed(3)} seconds`;
}
Logger.error(message, 'CoinGeckoService');
this.logger.error(message);
}
return response;
@ -262,7 +264,7 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
).toFixed(3)} seconds`;
}
Logger.error(message, 'CoinGeckoService');
this.logger.error(message);
}
return { items };

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

@ -11,6 +11,8 @@ 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',
@ -209,9 +211,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return undefined;
})
.catch(({ message }) => {
Logger.error(
`Failed to search Trackinsight symbol for ${symbol} (${message})`,
'TrackinsightDataEnhancerService'
this.logger.error(
`Failed to search Trackinsight symbol for ${symbol} (${message})`
);
return undefined;

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

@ -23,6 +23,8 @@ import type { Price } from 'yahoo-finance2/esm/src/modules/quoteSummary-iface';
@Injectable()
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
private readonly logger = new Logger(YahooFinanceDataEnhancerService.name);
private readonly yahooFinance = new YahooFinance({
suppressNotices: ['yahooSurvey']
});
@ -123,7 +125,7 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
response.url = url;
}
} catch (error) {
Logger.error(error, 'YahooFinanceDataEnhancerService');
this.logger.error(error);
}
return response;
@ -266,7 +268,7 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
`No data found, ${aSymbol} (${this.getName()}) may be delisted`
);
} else {
Logger.error(error, 'YahooFinanceService');
this.logger.error(error);
}
}

31
apps/api/src/services/data-provider/data-provider.service.ts

@ -41,6 +41,8 @@ import { AssetProfileInvalidError } from './errors/asset-profile-invalid.error';
@Injectable()
export class DataProviderService implements OnModuleInit {
private readonly logger = new Logger(DataProviderService.name);
private dataProviderMapping: { [dataProviderName: string]: string };
public constructor(
@ -129,7 +131,7 @@ export class DataProviderService implements OnModuleInit {
);
}
} catch (error) {
Logger.error(error, 'DataProviderService');
this.logger.error(error);
throw error;
}
@ -391,7 +393,7 @@ export class DataProviderService implements OnModuleInit {
return r;
}, {});
} catch (error) {
Logger.error(error, 'DataProviderService');
this.logger.error(error);
} finally {
return response;
}
@ -503,7 +505,7 @@ export class DataProviderService implements OnModuleInit {
result[symbol] = data;
}
} catch (error) {
Logger.error(error, 'DataProviderService');
this.logger.error(error);
throw error;
}
@ -567,13 +569,12 @@ export class DataProviderService implements OnModuleInit {
const numberOfItemsInCache = Object.keys(response)?.length;
if (numberOfItemsInCache) {
Logger.debug(
this.logger.debug(
`Fetched ${numberOfItemsInCache} quote${
numberOfItemsInCache > 1 ? 's' : ''
} from cache in ${((performance.now() - startTimeTotal) / 1000).toFixed(
3
)} seconds`,
'DataProviderService'
)} seconds`
);
}
@ -684,14 +685,13 @@ export class DataProviderService implements OnModuleInit {
}
}
Logger.debug(
this.logger.debug(
`Fetched ${symbolsChunk.length} quote${
symbolsChunk.length > 1 ? 's' : ''
} from ${dataSource} in ${(
(performance.now() - startTimeDataSource) /
1000
).toFixed(3)} seconds`,
'DataProviderService'
).toFixed(3)} seconds`
);
try {
@ -722,15 +722,18 @@ export class DataProviderService implements OnModuleInit {
await Promise.all(promises);
Logger.debug('--------------------------------------------------------');
Logger.debug(
this.logger.debug(
'--------------------------------------------------------'
);
this.logger.debug(
`Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${(
(performance.now() - startTimeTotal) /
1000
).toFixed(3)} seconds`,
'DataProviderService'
).toFixed(3)} seconds`
);
this.logger.debug(
'========================================================'
);
Logger.debug('========================================================');
return response;
}

21
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -37,6 +37,8 @@ import { isNumber } from 'lodash';
export class EodHistoricalDataService
implements DataProviderInterface, OnModuleInit
{
private readonly logger = new Logger(EodHistoricalDataService.name);
private apiKey: string;
private readonly URL = 'https://eodhistoricaldata.com/api';
@ -127,12 +129,11 @@ export class EodHistoricalDataService
return response;
} catch (error) {
Logger.error(
this.logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from,
DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`,
'EodHistoricalDataService'
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
);
return {};
@ -172,9 +173,8 @@ export class EodHistoricalDataService
marketPrice: adjusted_close
};
} else {
Logger.error(
`Could not get historical market data for ${symbol} (${this.getName()}) at ${date}`,
'EodHistoricalDataService'
this.logger.error(
`Could not get historical market data for ${symbol} (${this.getName()}) at ${date}`
);
}
@ -292,9 +292,8 @@ export class EodHistoricalDataService
dataSource: this.getName()
};
} else {
Logger.error(
`Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`,
'EodHistoricalDataService'
this.logger.error(
`Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`
);
}
}
@ -311,7 +310,7 @@ export class EodHistoricalDataService
).toFixed(3)} seconds`;
}
Logger.error(message, 'EodHistoricalDataService');
this.logger.error(message);
}
return {};
@ -465,7 +464,7 @@ export class EodHistoricalDataService
).toFixed(3)} seconds`;
}
Logger.error(message, 'EodHistoricalDataService');
this.logger.error(message);
}
return searchResult;

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

@ -49,6 +49,8 @@ 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',
@ -265,7 +267,7 @@ export class FinancialModelingPrepService
).toFixed(3)} seconds`;
}
Logger.error(message, 'FinancialModelingPrepService');
this.logger.error(message);
}
return response;
@ -325,12 +327,11 @@ export class FinancialModelingPrepService
return response;
} catch (error) {
Logger.error(
this.logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from,
DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`,
'FinancialModelingPrepService'
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
);
return {};
@ -518,7 +519,7 @@ export class FinancialModelingPrepService
).toFixed(3)} seconds`;
}
Logger.error(message, 'FinancialModelingPrepService');
this.logger.error(message);
}
return response;
@ -638,7 +639,7 @@ export class FinancialModelingPrepService
).toFixed(3)} seconds`;
}
Logger.error(message, 'FinancialModelingPrepService');
this.logger.error(message);
}
return { items };

12
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -33,6 +33,8 @@ import { StatusCodes } from 'http-status-codes';
@Injectable()
export class GhostfolioService implements DataProviderInterface {
private readonly logger = new Logger(GhostfolioService.name);
private readonly URL = environment.production
? 'https://ghostfol.io/api'
: `${this.configurationService.get('ROOT_URL')}/api`;
@ -89,7 +91,7 @@ export class GhostfolioService implements DataProviderInterface {
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
}
Logger.error(message, 'GhostfolioService');
this.logger.error(message);
}
return assetProfile;
@ -154,7 +156,7 @@ export class GhostfolioService implements DataProviderInterface {
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
}
Logger.error(message, 'GhostfolioService');
this.logger.error(message);
}
return dividends;
@ -211,7 +213,7 @@ export class GhostfolioService implements DataProviderInterface {
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
}
Logger.error(error.message, 'GhostfolioService');
this.logger.error(error.message);
throw new Error(
`Could not get historical market data for ${symbol} (${this.getName()}) from ${format(
@ -283,7 +285,7 @@ export class GhostfolioService implements DataProviderInterface {
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
}
Logger.error(message, 'GhostfolioService');
this.logger.error(message);
}
return quotes;
@ -338,7 +340,7 @@ export class GhostfolioService implements DataProviderInterface {
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
}
Logger.error(message, 'GhostfolioService');
this.logger.error(message);
}
return searchResult;

4
apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts

@ -24,6 +24,8 @@ import { GoogleSpreadsheet } from 'google-spreadsheet';
@Injectable()
export class GoogleSheetsService implements DataProviderInterface {
private readonly logger = new Logger(GoogleSheetsService.name);
public constructor(
private readonly configurationService: ConfigurationService,
private readonly prismaService: PrismaService,
@ -144,7 +146,7 @@ export class GoogleSheetsService implements DataProviderInterface {
return response;
} catch (error) {
Logger.error(error, 'GoogleSheetsService');
this.logger.error(error);
}
return {};

9
apps/api/src/services/data-provider/manual/manual.service.ts

@ -31,6 +31,8 @@ import { addDays, format, isBefore } from 'date-fns';
@Injectable()
export class ManualService implements DataProviderInterface {
private readonly logger = new Logger(ManualService.name);
public constructor(
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
@ -181,9 +183,8 @@ export class ManualService implements DataProviderInterface {
});
return { marketPrice, symbol };
} catch (error) {
Logger.error(
`Could not get quote for ${symbol} (${this.getName()}): [${error.name}] ${error.message}`,
'ManualService'
this.logger.error(
`Could not get quote for ${symbol} (${this.getName()}): [${error.name}] ${error.message}`
);
return { symbol, marketPrice: undefined };
}
@ -216,7 +217,7 @@ export class ManualService implements DataProviderInterface {
return response;
} catch (error) {
Logger.error(error, 'ManualService');
this.logger.error(error);
}
return {};

6
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -26,6 +26,8 @@ import { format } from 'date-fns';
@Injectable()
export class RapidApiService implements DataProviderInterface {
private readonly logger = new Logger(RapidApiService.name);
public constructor(
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService
@ -122,7 +124,7 @@ export class RapidApiService implements DataProviderInterface {
};
}
} catch (error) {
Logger.error(error, 'RapidApiService');
this.logger.error(error);
}
return {};
@ -167,7 +169,7 @@ export class RapidApiService implements DataProviderInterface {
).toFixed(3)} seconds`;
}
Logger.error(message, 'RapidApiService');
this.logger.error(message);
return undefined;
}

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

@ -41,6 +41,8 @@ import { SearchQuoteNonYahoo } from 'yahoo-finance2/esm/src/modules/search';
@Injectable()
export class YahooFinanceService implements DataProviderInterface {
private readonly logger = new Logger(YahooFinanceService.name);
private readonly yahooFinance = new YahooFinance({
suppressNotices: ['yahooSurvey']
});
@ -105,12 +107,11 @@ export class YahooFinanceService implements DataProviderInterface {
return response;
} catch (error) {
Logger.error(
this.logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from,
DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`,
'YahooFinanceService'
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
);
return {};
@ -198,12 +199,9 @@ export class YahooFinanceService implements DataProviderInterface {
try {
quotes = await this.yahooFinance.quote(yahooFinanceSymbols);
} catch (error) {
Logger.error(error, 'YahooFinanceService');
this.logger.error(error);
Logger.warn(
'Fallback to yahooFinance.quoteSummary()',
'YahooFinanceService'
);
this.logger.warn('Fallback to yahooFinance.quoteSummary()');
quotes = await this.getQuotesWithQuoteSummary(yahooFinanceSymbols);
}
@ -229,7 +227,7 @@ export class YahooFinanceService implements DataProviderInterface {
return response;
} catch (error) {
Logger.error(error, 'YahooFinanceService');
this.logger.error(error);
return {};
}
@ -334,7 +332,7 @@ export class YahooFinanceService implements DataProviderInterface {
});
}
} catch (error) {
Logger.error(error, 'YahooFinanceService');
this.logger.error(error);
}
return { items };
@ -365,10 +363,7 @@ export class YahooFinanceService implements DataProviderInterface {
.filter(
(result): result is PromiseFulfilledResult<QuoteSummaryResult> => {
if (result.status === 'rejected') {
Logger.error(
`Could not get quote summary: ${result.reason}`,
'YahooFinanceService'
);
this.logger.error(`Could not get quote summary: ${result.reason}`);
return false;
}

19
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -30,6 +30,8 @@ import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interfa
@Injectable()
export class ExchangeRateDataService {
private readonly logger = new Logger(ExchangeRateDataService.name);
private currencies: string[] = [];
private currencyPairs: DataGatheringItem[] = [];
private derivedCurrencyFactors: { [currencyPair: string]: number } = {};
@ -110,9 +112,8 @@ export class ExchangeRateDataService {
previousExchangeRate;
if (currency === DEFAULT_CURRENCY && isBefore(date, new Date())) {
Logger.error(
`No exchange rate has been found for ${currency}${targetCurrency} at ${dateString}`,
'ExchangeRateDataService'
this.logger.error(
`No exchange rate has been found for ${currency}${targetCurrency} at ${dateString}`
);
}
} else {
@ -253,9 +254,8 @@ export class ExchangeRateDataService {
}
// Fallback with error, if currencies are not available
Logger.error(
`No exchange rate has been found for ${aFromCurrency}${aToCurrency}`,
'ExchangeRateDataService'
this.logger.error(
`No exchange rate has been found for ${aFromCurrency}${aToCurrency}`
);
return aValue;
@ -341,12 +341,11 @@ export class ExchangeRateDataService {
return factor * aValue;
}
Logger.error(
this.logger.error(
`No exchange rate has been found for ${aFromCurrency}${aToCurrency} at ${format(
aDate,
DATE_FORMAT
)}`,
'ExchangeRateDataService'
)}`
);
return undefined;
@ -483,7 +482,7 @@ export class ExchangeRateDataService {
errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`;
}
Logger.error(`${errorMessage}.`, 'ExchangeRateDataService');
this.logger.error(`${errorMessage}.`);
}
}
}

24
apps/api/src/services/fetch/fetch.service.ts

@ -15,6 +15,8 @@ import { WebFetchRoute } from './interfaces/web-fetch-route.interface';
@Injectable()
export class FetchService implements OnModuleInit {
private readonly logger = new Logger(FetchService.name);
private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token'];
private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds');
@ -39,7 +41,7 @@ export class FetchService implements OnModuleInit {
const url = input instanceof Request ? input.url : input.toString();
const urlRedacted = this.redactUrl(url);
Logger.debug(`${method} ${urlRedacted}`, 'FetchService');
this.logger.debug(`${method} ${urlRedacted}`);
if (method === 'GET') {
const webFetchRoute = this.getMatchingWebFetchRoute(url);
@ -60,15 +62,11 @@ export class FetchService implements OnModuleInit {
return await globalThis.fetch(input, init);
} catch (error) {
if (error instanceof Error) {
Logger.error(
`${method} ${urlRedacted} failed: [${error.name}] ${error.message}`,
'FetchService'
this.logger.error(
`${method} ${urlRedacted} failed: [${error.name}] ${error.message}`
);
} else {
Logger.error(
`${method} ${urlRedacted} failed: ${String(error)}`,
'FetchService'
);
this.logger.error(`${method} ${urlRedacted} failed: ${String(error)}`);
}
throw error;
@ -145,10 +143,7 @@ export class FetchService implements OnModuleInit {
}
}
Logger.debug(
`Routed ${this.redactUrl(url)} via web fetch tool`,
'FetchService'
);
this.logger.debug(`Routed ${this.redactUrl(url)} via web fetch tool`);
return new Response(body, {
headers: webFetchRoute.responseContentType
@ -159,11 +154,10 @@ export class FetchService implements OnModuleInit {
return undefined;
} catch (error) {
Logger.error(
this.logger.error(
`Web fetch tool failed for ${this.redactUrl(url)}: ${
error instanceof Error ? error.message : String(error)
}`,
'FetchService'
}`
);
return undefined;

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

@ -7,6 +7,8 @@ import { join } from 'node:path';
@Injectable()
export class I18nService implements OnModuleInit {
private readonly logger = new Logger(I18nService.name);
private localesPath = join(__dirname, 'assets', 'locales');
private translations: { [locale: string]: cheerio.CheerioAPI } = {};
@ -26,7 +28,7 @@ export class I18nService implements OnModuleInit {
const $ = this.translations[languageCode];
if (!$) {
Logger.warn(`Translation not found for locale '${languageCode}'`);
this.logger.warn(`Translation not found for locale '${languageCode}'`);
}
let translatedText = $(
@ -36,7 +38,7 @@ export class I18nService implements OnModuleInit {
).text();
if (!translatedText) {
Logger.warn(
this.logger.warn(
`Translation not found for id '${id}' in locale '${languageCode}'`
);
}
@ -60,7 +62,7 @@ export class I18nService implements OnModuleInit {
this.parseXml(xmlData);
}
} catch (error) {
Logger.error(error, 'I18nService');
this.logger.error(error);
}
}

4
apps/api/src/services/prisma/prisma.service.ts

@ -14,6 +14,8 @@ export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
private readonly logger = new Logger(PrismaService.name);
public constructor(configService: ConfigService) {
const adapter = new PrismaPg({
connectionString: configService.get<string>('DATABASE_URL')
@ -43,7 +45,7 @@ export class PrismaService
try {
await this.$connect();
} catch (error) {
Logger.error(error, 'PrismaService');
this.logger.error(error);
}
}

42
apps/api/src/services/queues/data-gathering/data-gathering.processor.ts

@ -32,6 +32,8 @@ import { DataGatheringService } from './data-gathering.service';
@Injectable()
@Processor(DATA_GATHERING_QUEUE)
export class DataGatheringProcessor {
private readonly logger = new Logger(DataGatheringProcessor.name);
public constructor(
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService,
@ -51,16 +53,14 @@ export class DataGatheringProcessor {
const { dataSource, symbol } = job.data;
try {
Logger.log(
`Asset profile data gathering has been started for ${symbol} (${dataSource})`,
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
this.logger.log(
`Asset profile data gathering has been started for ${symbol} (${dataSource})`
);
await this.dataGatheringService.gatherAssetProfiles([job.data]);
Logger.log(
`Asset profile data gathering has been completed for ${symbol} (${dataSource})`,
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
this.logger.log(
`Asset profile data gathering has been completed for ${symbol} (${dataSource})`
);
} catch (error) {
if (error instanceof AssetProfileDelistedError) {
@ -74,18 +74,14 @@ export class DataGatheringProcessor {
}
);
Logger.log(
`Asset profile data gathering has been discarded for ${symbol} (${dataSource})`,
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
this.logger.log(
`Asset profile data gathering has been discarded for ${symbol} (${dataSource})`
);
return job.discard();
}
Logger.error(
error,
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
);
this.logger.error(error);
throw error;
}
@ -105,12 +101,11 @@ export class DataGatheringProcessor {
try {
let currentDate = parseISO(date as unknown as string);
Logger.log(
this.logger.log(
`Historical market data gathering has been started for ${symbol} (${dataSource}) at ${format(
currentDate,
DATE_FORMAT
)}${force ? ' (forced update)' : ''}`,
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
)}${force ? ' (forced update)' : ''}`
);
const historicalData = await this.dataProviderService.getHistoricalRaw({
@ -167,12 +162,11 @@ export class DataGatheringProcessor {
await this.marketDataService.updateMany({ data });
}
Logger.log(
this.logger.log(
`Historical market data gathering has been completed for ${symbol} (${dataSource}) at ${format(
currentDate,
DATE_FORMAT
)}`,
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
)}`
);
} catch (error) {
if (error instanceof AssetProfileDelistedError) {
@ -186,18 +180,14 @@ export class DataGatheringProcessor {
}
);
Logger.log(
`Historical market data gathering has been discarded for ${symbol} (${dataSource})`,
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
this.logger.log(
`Historical market data gathering has been discarded for ${symbol} (${dataSource})`
);
return job.discard();
}
Logger.error(
error,
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
);
this.logger.error(error);
throw error;
}

15
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -34,6 +34,8 @@ import ms, { StringValue } from 'ms';
@Injectable()
export class DataGatheringService {
private readonly logger = new Logger(DataGatheringService.name);
public constructor(
@Inject('DataEnhancers')
private readonly dataEnhancers: DataEnhancerInterface[],
@ -145,7 +147,7 @@ export class DataGatheringService {
});
}
} catch (error) {
Logger.error(error, 'DataGatheringService');
this.logger.error(error);
} finally {
return undefined;
}
@ -200,12 +202,11 @@ export class DataGatheringService {
symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol
});
} catch (error) {
Logger.error(
this.logger.error(
`Failed to enhance data for ${symbol} (${
assetProfile.dataSource
}) by ${dataEnhancer.getName()}`,
error,
'DataGatheringService'
error
);
}
}
@ -269,11 +270,7 @@ export class DataGatheringService {
}
});
} catch (error) {
Logger.error(
`${symbol}: ${error?.meta?.cause}`,
error,
'DataGatheringService'
);
this.logger.error(`${symbol}: ${error?.meta?.cause}`, error);
if (assetProfileIdentifiers.length === 1) {
throw error;

17
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts

@ -21,6 +21,8 @@ import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue
@Injectable()
@Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE)
export class PortfolioSnapshotProcessor {
private readonly logger = new Logger(PortfolioSnapshotProcessor.name);
public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly activitiesService: ActivitiesService,
@ -41,9 +43,8 @@ export class PortfolioSnapshotProcessor {
try {
const startTime = performance.now();
Logger.log(
`Portfolio snapshot calculation of user '${job.data.userId}' has been started`,
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
this.logger.log(
`Portfolio snapshot calculation of user '${job.data.userId}' has been started`
);
const { activities } =
@ -72,12 +73,11 @@ export class PortfolioSnapshotProcessor {
const snapshot = await portfolioCalculator.computeSnapshot();
Logger.log(
this.logger.log(
`Portfolio snapshot calculation of user '${job.data.userId}' has been completed in ${(
(performance.now() - startTime) /
1000
).toFixed(3)} seconds`,
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
).toFixed(3)} seconds`
);
const expiration = addMilliseconds(
@ -101,10 +101,7 @@ export class PortfolioSnapshotProcessor {
return snapshot;
} catch (error) {
Logger.error(
error,
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
);
this.logger.error(error);
throw new Error(error);
}

55
apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts

@ -27,6 +27,8 @@ import { format, subDays } from 'date-fns';
@Injectable()
@Processor(STATISTICS_GATHERING_QUEUE)
export class StatisticsGatheringProcessor {
private readonly logger = new Logger(StatisticsGatheringProcessor.name);
public constructor(
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
@ -35,10 +37,7 @@ export class StatisticsGatheringProcessor {
@Process(GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME)
public async gatherDockerHubPullsStatistics() {
Logger.log(
'Docker Hub pulls statistics gathering has been started',
'StatisticsGatheringProcessor'
);
this.logger.log('Docker Hub pulls statistics gathering has been started');
const dockerHubPulls = await this.countDockerHubPulls();
@ -47,17 +46,13 @@ export class StatisticsGatheringProcessor {
value: String(dockerHubPulls)
});
Logger.log(
'Docker Hub pulls statistics gathering has been completed',
'StatisticsGatheringProcessor'
);
this.logger.log('Docker Hub pulls statistics gathering has been completed');
}
@Process(GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME)
public async gatherGitHubContributorsStatistics() {
Logger.log(
'GitHub contributors statistics gathering has been started',
'StatisticsGatheringProcessor'
this.logger.log(
'GitHub contributors statistics gathering has been started'
);
const gitHubContributors = await this.countGitHubContributors();
@ -67,18 +62,14 @@ export class StatisticsGatheringProcessor {
value: String(gitHubContributors)
});
Logger.log(
'GitHub contributors statistics gathering has been completed',
'StatisticsGatheringProcessor'
this.logger.log(
'GitHub contributors statistics gathering has been completed'
);
}
@Process(GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME)
public async gatherGitHubStargazersStatistics() {
Logger.log(
'GitHub stargazers statistics gathering has been started',
'StatisticsGatheringProcessor'
);
this.logger.log('GitHub stargazers statistics gathering has been started');
const gitHubStargazers = await this.countGitHubStargazers();
@ -87,9 +78,8 @@ export class StatisticsGatheringProcessor {
value: String(gitHubStargazers)
});
Logger.log(
'GitHub stargazers statistics gathering has been completed',
'StatisticsGatheringProcessor'
this.logger.log(
'GitHub stargazers statistics gathering has been completed'
);
}
@ -100,18 +90,14 @@ export class StatisticsGatheringProcessor {
);
if (!monitorId) {
Logger.log(
`Uptime statistics gathering has been skipped as no ${PROPERTY_BETTER_UPTIME_MONITOR_ID} is configured`,
'StatisticsGatheringProcessor'
this.logger.log(
`Uptime statistics gathering has been skipped as no ${PROPERTY_BETTER_UPTIME_MONITOR_ID} is configured`
);
return;
}
Logger.log(
'Uptime statistics gathering has been started',
'StatisticsGatheringProcessor'
);
this.logger.log('Uptime statistics gathering has been started');
const uptime = await this.getUptime(monitorId);
@ -120,10 +106,7 @@ export class StatisticsGatheringProcessor {
value: String(uptime)
});
Logger.log(
'Uptime statistics gathering has been completed',
'StatisticsGatheringProcessor'
);
this.logger.log('Uptime statistics gathering has been completed');
}
private async countDockerHubPulls(): Promise<number> {
@ -139,7 +122,7 @@ export class StatisticsGatheringProcessor {
return pull_count;
} catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - DockerHub');
this.logger.error(error);
throw error;
}
@ -169,7 +152,7 @@ export class StatisticsGatheringProcessor {
value
});
} catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - GitHub');
this.logger.error(error);
throw error;
}
@ -188,7 +171,7 @@ export class StatisticsGatheringProcessor {
return stargazers_count;
} catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - GitHub');
this.logger.error(error);
throw error;
}
@ -217,7 +200,7 @@ export class StatisticsGatheringProcessor {
return data.attributes.availability / 100;
} catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - Better Stack');
this.logger.error(error);
throw error;
}

9
apps/api/src/services/twitter-bot/twitter-bot.service.ts

@ -16,6 +16,8 @@ import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2';
@Injectable()
export class TwitterBotService implements OnModuleInit {
private readonly logger = new Logger(TwitterBotService.name);
private twitterClient: TwitterApiReadWrite;
public constructor(
@ -71,13 +73,12 @@ export class TwitterBotService implements OnModuleInit {
const { data: createdTweet } =
await this.twitterClient.v2.tweet(status);
Logger.log(
`Fear & Greed Index has been posted: https://x.com/ghostfolio_/status/${createdTweet.id}`,
'TwitterBotService'
this.logger.log(
`Fear & Greed Index has been posted: https://x.com/ghostfolio_/status/${createdTweet.id}`
);
}
} catch (error) {
Logger.error(error, 'TwitterBotService');
this.logger.error(error);
}
}

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

@ -292,7 +292,7 @@
</trans-unit>
<trans-unit id="9153520284278555926" datatype="html">
<source>please</source>
<target state="new">please</target>
<target state="translated">будь ласка</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context>
<context context-type="linenumber">333</context>
@ -360,7 +360,7 @@
</trans-unit>
<trans-unit id="1351814922314683865" datatype="html">
<source>with</source>
<target state="new">with</target>
<target state="translated">з</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html</context>
<context context-type="linenumber">87</context>
@ -1388,7 +1388,7 @@
</trans-unit>
<trans-unit id="7702646444963497962" datatype="html">
<source>By</source>
<target state="new">By</target>
<target state="translated">До</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">139</context>
@ -1892,7 +1892,7 @@
</trans-unit>
<trans-unit id="5303806780432428245" datatype="html">
<source>Indonesia</source>
<target state="new">Indonesia</target>
<target state="translated">Індонезія</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">90</context>
@ -2108,7 +2108,7 @@
</trans-unit>
<trans-unit id="8186013988289067040" datatype="html">
<source>Code</source>
<target state="new">Code</target>
<target state="translated">Код</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">159</context>
@ -2636,7 +2636,7 @@
</trans-unit>
<trans-unit id="2575998129003872734" datatype="html">
<source>Argentina</source>
<target state="new">Argentina</target>
<target state="translated">Аргентина</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">78</context>
@ -3204,7 +3204,7 @@
</trans-unit>
<trans-unit id="8553460997100418147" datatype="html">
<source>for</source>
<target state="new">for</target>
<target state="translated">для</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html</context>
<context context-type="linenumber">128</context>
@ -3560,7 +3560,7 @@
</trans-unit>
<trans-unit id="7410432243549869948" datatype="html">
<source>Duration</source>
<target state="new">Duration</target>
<target state="translated">Тривалість</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">172</context>
@ -4897,7 +4897,7 @@
</trans-unit>
<trans-unit id="858192247408211331" datatype="html">
<source>here</source>
<target state="new">here</target>
<target state="translated">тут</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context>
<context context-type="linenumber">347</context>
@ -5113,7 +5113,7 @@
</trans-unit>
<trans-unit id="6962217007874959362" datatype="html">
<source>Our official Ghostfolio Premium cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. Revenue is used to cover operational costs for the hosting infrastructure and professional data providers, and to fund ongoing development.</source>
<target state="new">Наша офіційна хмарна пропозиція Ghostfolio Premium - це найпростіший спосіб почати роботу. Завдяки економії часу, це буде найкращим варіантом для більшості людей. Доходи використовуються для покриття витрат на хостинг-інфраструктуру та фінансування постійної розробки.</target>
<target state="translated">Наша офіційна хмарна пропозиція Ghostfolio Premium - це найпростіший спосіб почати роботу. Завдяки економії часу, це буде найкращим варіантом для більшості людей. Доходи використовуються для покриття витрат на хостинг-інфраструктуру та фінансування постійної розробки.</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context>
<context context-type="linenumber">7</context>
@ -6392,7 +6392,7 @@
</trans-unit>
<trans-unit id="5515771028435710194" datatype="html">
<source>Loan</source>
<target state="new">Loan</target>
<target state="translated">Позика</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">58</context>
@ -6944,7 +6944,7 @@
</trans-unit>
<trans-unit id="4145496584631696119" datatype="html">
<source>Role</source>
<target state="new">Role</target>
<target state="translated">Роль</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html</context>
<context context-type="linenumber">39</context>
@ -7080,7 +7080,7 @@
</trans-unit>
<trans-unit id="8966698274727122602" datatype="html">
<source>Authentication</source>
<target state="new">Authentication</target>
<target state="translated">Автентифікація</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html</context>
<context context-type="linenumber">60</context>
@ -7476,7 +7476,7 @@
</trans-unit>
<trans-unit id="8540986733881734625" datatype="html">
<source>Lazy</source>
<target state="new">Lazy</target>
<target state="translated">Лінивий</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">235</context>
@ -7484,7 +7484,7 @@
</trans-unit>
<trans-unit id="6882618704933649036" datatype="html">
<source>Instant</source>
<target state="new">Instant</target>
<target state="translated">Миттєвий</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">239</context>
@ -7500,7 +7500,7 @@
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
<source>Mode</source>
<target state="new">Mode</target>
<target state="translated">Режим</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">519</context>
@ -7508,7 +7508,7 @@
</trans-unit>
<trans-unit id="3540108566782816830" datatype="html">
<source>Selector</source>
<target state="new">Selector</target>
<target state="translated">Селектор</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">535</context>
@ -7532,7 +7532,7 @@
</trans-unit>
<trans-unit id="4547068148181074902" datatype="html">
<source>real-time</source>
<target state="new">real-time</target>
<target state="translated">реальний час</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">239</context>
@ -7548,7 +7548,7 @@
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">
<source>Create</source>
<target state="new">Create</target>
<target state="translated">Створити</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/tags-selector/tags-selector.component.html</context>
<context context-type="linenumber">50</context>
@ -7556,7 +7556,7 @@
</trans-unit>
<trans-unit id="1230154438678955604" datatype="html">
<source>Change</source>
<target state="new">Change</target>
<target state="translated">Змінити</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/holdings-table/holdings-table.component.html</context>
<context context-type="linenumber">138</context>
@ -7568,7 +7568,7 @@
</trans-unit>
<trans-unit id="1322586333669103999" datatype="html">
<source>Performance</source>
<target state="new">Performance</target>
<target state="translated">Дохідність</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">6</context>
@ -7624,7 +7624,7 @@
</trans-unit>
<trans-unit id="5004849258025239958" datatype="html">
<source>Armenia</source>
<target state="new">Armenia</target>
<target state="translated">Вірменія</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">77</context>
@ -7640,7 +7640,7 @@
</trans-unit>
<trans-unit id="4830118002486243553" datatype="html">
<source>Singapore</source>
<target state="new">Singapore</target>
<target state="translated">Сінгапур</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">97</context>
@ -7672,7 +7672,7 @@
</trans-unit>
<trans-unit id="6962699013778688473" datatype="html">
<source>Continue</source>
<target state="new">Continue</target>
<target state="translated">Продовжити</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html</context>
<context context-type="linenumber">57</context>
@ -7732,7 +7732,7 @@
</trans-unit>
<trans-unit id="routes.about.termsOfService" datatype="html">
<source>terms-of-service</source>
<target state="new">terms-of-service</target>
<target state="translated">umovy-nadannia-posluh</target>
<note priority="1" from="description">kebab-case</note>
<context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
@ -7781,7 +7781,7 @@
</trans-unit>
<trans-unit id="4391289919356861627" datatype="html">
<source>Apply</source>
<target state="new">Apply</target>
<target state="translated">Застосувати</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">154</context>
@ -7837,7 +7837,7 @@
</trans-unit>
<trans-unit id="322229249598827754" datatype="html">
<source>someone</source>
<target state="new">someone</target>
<target state="translated">когось</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.component.ts</context>
<context context-type="linenumber">62</context>
@ -7853,7 +7853,7 @@
</trans-unit>
<trans-unit id="4558213855845176930" datatype="html">
<source>Watchlist</source>
<target state="new">Watchlist</target>
<target state="translated">Список спостереження</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-watchlist/home-watchlist.html</context>
<context context-type="linenumber">4</context>
@ -7897,7 +7897,7 @@
</trans-unit>
<trans-unit id="routes.about.changelog" datatype="html">
<source>changelog</source>
<target state="new">changelog</target>
<target state="translated">zhurnal-zmin</target>
<note priority="1" from="description">kebab-case</note>
<context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
@ -8030,7 +8030,7 @@
</trans-unit>
<trans-unit id="routes.resources.personalFinanceTools" datatype="html">
<source>personal-finance-tools</source>
<target state="new">personal-finance-tools</target>
<target state="translated">instrumenty-osobystykh-finansiv</target>
<note priority="1" from="description">kebab-case</note>
<context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
@ -8047,7 +8047,7 @@
</trans-unit>
<trans-unit id="routes.resources.markets" datatype="html">
<source>markets</source>
<target state="new">markets</target>
<target state="translated">rynky</target>
<note priority="1" from="description">kebab-case</note>
<context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
@ -8108,7 +8108,7 @@
</trans-unit>
<trans-unit id="3955868613858648955" datatype="html">
<source>Available</source>
<target state="new">Available</target>
<target state="translated">Доступно</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/data-provider-status/data-provider-status.component.html</context>
<context context-type="linenumber">3</context>
@ -8116,7 +8116,7 @@
</trans-unit>
<trans-unit id="5643561794785412000" datatype="html">
<source>Unavailable</source>
<target state="new">Unavailable</target>
<target state="translated">Недоступно</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/data-provider-status/data-provider-status.component.html</context>
<context context-type="linenumber">5</context>
@ -8132,7 +8132,7 @@
</trans-unit>
<trans-unit id="7387635272539030076" datatype="html">
<source>new</source>
<target state="new">new</target>
<target state="translated">новий</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-settings/admin-settings.component.html</context>
<context context-type="linenumber">79</context>
@ -8140,7 +8140,7 @@
</trans-unit>
<trans-unit id="rule.accountClusterRiskCurrentInvestment" datatype="html">
<source>Investment</source>
<target state="new">Investment</target>
<target state="translated">Інвестиція</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">15</context>
@ -8164,7 +8164,7 @@
</trans-unit>
<trans-unit id="rule.assetClassClusterRiskEquity" datatype="html">
<source>Equity</source>
<target state="new">Equity</target>
<target state="translated">Акції</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">41</context>
@ -8252,7 +8252,7 @@
</trans-unit>
<trans-unit id="rule.currencyClusterRiskCurrentInvestment" datatype="html">
<source>Investment</source>
<target state="new">Investment</target>
<target state="translated">Інвестиція</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">95</context>
@ -8276,7 +8276,7 @@
</trans-unit>
<trans-unit id="routes.start" datatype="html">
<source>start</source>
<target state="new">start</target>
<target state="translated">pochatok</target>
<note priority="1" from="description">kebab-case</note>
<context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
@ -8297,7 +8297,7 @@
</trans-unit>
<trans-unit id="5193539160604294602" datatype="html">
<source>Generate</source>
<target state="new">Generate</target>
<target state="translated">Згенерувати</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.html</context>
<context context-type="linenumber">45</context>
@ -8313,7 +8313,7 @@
</trans-unit>
<trans-unit id="2732382861635368484" datatype="html">
<source>Stocks</source>
<target state="new">Stocks</target>
<target state="translated">Акції</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/markets/markets.component.ts</context>
<context context-type="linenumber">51</context>
@ -8325,7 +8325,7 @@
</trans-unit>
<trans-unit id="1419479195323304896" datatype="html">
<source>Cryptocurrencies</source>
<target state="new">Cryptocurrencies</target>
<target state="translated">Криптовалюти</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/markets/markets.component.ts</context>
<context context-type="linenumber">52</context>
@ -8361,7 +8361,7 @@
</trans-unit>
<trans-unit id="5795124554973640871" datatype="html">
<source>Collectible</source>
<target state="new">Collectible</target>
<target state="translated">Колекційний предмет</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">55</context>
@ -8421,7 +8421,7 @@
</trans-unit>
<trans-unit id="rule.fees.category" datatype="html">
<source>Fees</source>
<target state="new">Fees</target>
<target state="translated">Комісії</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">161</context>
@ -8429,7 +8429,7 @@
</trans-unit>
<trans-unit id="rule.liquidity.category" datatype="html">
<source>Liquidity</source>
<target state="new">Liquidity</target>
<target state="translated">Ліквідність</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">70</context>
@ -8565,7 +8565,7 @@
</trans-unit>
<trans-unit id="rule.regionalMarketClusterRiskAsiaPacific" datatype="html">
<source>Asia-Pacific</source>
<target state="new">Asia-Pacific</target>
<target state="translated">Азіатсько-Тихоокеанський регіон</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">165</context>
@ -8629,7 +8629,7 @@
</trans-unit>
<trans-unit id="rule.regionalMarketClusterRiskEurope" datatype="html">
<source>Europe</source>
<target state="new">Europe</target>
<target state="translated">Європа</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">195</context>
@ -8661,7 +8661,7 @@
</trans-unit>
<trans-unit id="rule.regionalMarketClusterRiskJapan" datatype="html">
<source>Japan</source>
<target state="new">Japan</target>
<target state="translated">Японія</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">209</context>

Loading…
Cancel
Save