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 ### Changed
- Centralized the asset profile override logic for manual adjustments - 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 ### Fixed

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

@ -58,6 +58,8 @@ import { AdminService } from './admin.service';
@Controller('admin') @Controller('admin')
export class AdminController { export class AdminController {
private readonly logger = new Logger(AdminController.name);
public constructor( public constructor(
private readonly adminService: AdminService, private readonly adminService: AdminService,
private readonly apiService: ApiService, private readonly apiService: ApiService,
@ -260,7 +262,7 @@ export class AdminController {
`Could not parse the market price for ${symbol} (${dataSource})` `Could not parse the market price for ${symbol} (${dataSource})`
); );
} catch (error) { } catch (error) {
Logger.error(error, 'AdminController'); this.logger.error(error);
throw new HttpException(error.message, StatusCodes.BAD_REQUEST); 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, configurationService: ConfigurationService,
fetchService: FetchService fetchService: FetchService
) => { ) => {
const logger = new Logger('OidcStrategy');
const isOidcEnabled = configurationService.get( const isOidcEnabled = configurationService.get(
'ENABLE_FEATURE_AUTH_OIDC' 'ENABLE_FEATURE_AUTH_OIDC'
); );
@ -101,7 +103,7 @@ import { OidcStrategy } from './oidc.strategy';
tokenURL = manualTokenUrl || config.token_endpoint; tokenURL = manualTokenUrl || config.token_endpoint;
userInfoURL = manualUserInfoUrl || config.userinfo_endpoint; userInfoURL = manualUserInfoUrl || config.userinfo_endpoint;
} catch (error) { } catch (error) {
Logger.error(error, 'OidcStrategy'); logger.error(error);
throw new Error('Failed to fetch OIDC configuration from issuer'); 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() @Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
private readonly logger = new Logger(GoogleStrategy.name);
public constructor( public constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
configurationService: ConfigurationService configurationService: ConfigurationService
@ -40,7 +42,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
done(null, { jwt }); done(null, { jwt });
} catch (error) { } catch (error) {
Logger.error(error, 'GoogleStrategy'); this.logger.error(error);
done(error, false); done(error, false);
} }
} }

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

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

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

@ -33,6 +33,8 @@ import ms from 'ms';
@Injectable() @Injectable()
export class WebAuthService { export class WebAuthService {
private readonly logger = new Logger(WebAuthService.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly deviceService: AuthDeviceService, private readonly deviceService: AuthDeviceService,
@ -103,7 +105,7 @@ export class WebAuthService {
verification = await verifyRegistrationResponse(opts); verification = await verifyRegistrationResponse(opts);
} catch (error) { } catch (error) {
Logger.error(error, 'WebAuthService'); this.logger.error(error);
throw new InternalServerErrorException(error.message); throw new InternalServerErrorException(error.message);
} }
@ -210,7 +212,7 @@ export class WebAuthService {
verification = await verifyAuthenticationResponse(opts); verification = await verifyAuthenticationResponse(opts);
} catch (error) { } catch (error) {
Logger.error(error, 'WebAuthService'); this.logger.error(error);
throw new InternalServerErrorException({ error: error.message }); 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() @Injectable()
export class BenchmarksService { export class BenchmarksService {
private readonly logger = new Logger(BenchmarksService.name);
public constructor( public constructor(
private readonly benchmarkService: BenchmarkService, private readonly benchmarkService: BenchmarkService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
@ -96,12 +98,11 @@ export class BenchmarksService {
})?.marketPrice; })?.marketPrice;
if (!marketPriceAtStartDate) { if (!marketPriceAtStartDate) {
Logger.error( this.logger.error(
`No historical market data has been found for ${symbol} (${dataSource}) at ${format( `No historical market data has been found for ${symbol} (${dataSource}) at ${format(
startDate, startDate,
DATE_FORMAT DATE_FORMAT
)}`, )}`
'BenchmarkService'
); );
return { marketData }; return { marketData };

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

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

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

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

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

@ -31,6 +31,8 @@ import { ImportService } from './import.service';
@Controller('import') @Controller('import')
export class ImportController { export class ImportController {
private readonly logger = new Logger(ImportController.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly importService: ImportService, private readonly importService: ImportService,
@ -81,7 +83,7 @@ export class ImportController {
return { activities }; return { activities };
} catch (error) { } catch (error) {
Logger.error(error, ImportController); this.logger.error(error);
throw new HttpException( 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 { export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false; protected static readonly ENABLE_LOGGING = false;
protected readonly logger = new Logger(PortfolioCalculator.name);
protected accountBalanceItems: HistoricalDataItem[]; protected accountBalanceItems: HistoricalDataItem[];
protected activities: PortfolioOrder[]; protected activities: PortfolioOrder[];
@ -1119,12 +1121,11 @@ export abstract class PortfolioCalculator {
if (cachedPortfolioSnapshot) { if (cachedPortfolioSnapshot) {
this.snapshot = cachedPortfolioSnapshot; this.snapshot = cachedPortfolioSnapshot;
Logger.debug( this.logger.debug(
`Fetched portfolio snapshot from cache in ${( `Fetched portfolio snapshot from cache in ${(
(performance.now() - startTimeTotal) / (performance.now() - startTimeTotal) /
1000 1000
).toFixed(3)} seconds`, ).toFixed(3)} seconds`
'PortfolioCalculator'
); );
if (isCachedPortfolioSnapshotExpired) { 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 { DateRange } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Logger } from '@nestjs/common';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { import {
addMilliseconds, addMilliseconds,
@ -96,9 +95,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
currentPosition.timeWeightedInvestmentWithCurrencyEffect currentPosition.timeWeightedInvestmentWithCurrencyEffect
); );
} else if (!currentPosition.quantity.eq(0)) { } else if (!currentPosition.quantity.eq(0)) {
Logger.warn( this.logger.warn(
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`, `Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`
'PortfolioCalculator'
); );
hasErrors = true; hasErrors = true;

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

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

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

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

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

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

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

@ -24,6 +24,8 @@ import Stripe from 'stripe';
@Injectable() @Injectable()
export class SubscriptionService { export class SubscriptionService {
private readonly logger = new Logger(SubscriptionService.name);
private stripe: Stripe; private stripe: Stripe;
public constructor( public constructor(
@ -166,9 +168,8 @@ export class SubscriptionService {
error instanceof Prisma.PrismaClientKnownRequestError && error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002' error.code === 'P2002'
) { ) {
Logger.log( this.logger.log(
`Stripe Checkout Session '${session.id}' has already been redeemed`, `Stripe Checkout Session '${session.id}' has already been redeemed`
'SubscriptionService'
); );
} else { } else {
throw error; throw error;
@ -177,7 +178,7 @@ export class SubscriptionService {
return session.client_reference_id; return session.client_reference_id;
} catch (error) { } 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() @Injectable()
export class SymbolService { export class SymbolService {
private readonly logger = new Logger(SymbolService.name);
public constructor( public constructor(
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService private readonly marketDataService: MarketDataService
@ -119,7 +121,7 @@ export class SymbolService {
results.items = items; results.items = items;
return results; return results;
} catch (error) { } catch (error) {
Logger.error(error, 'SymbolService'); this.logger.error(error);
throw 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() @Injectable()
export class AssetProfileChangedListener { export class AssetProfileChangedListener {
private readonly logger = new Logger(AssetProfileChangedListener.name);
private static readonly DEBOUNCE_DELAY = ms('5 seconds'); private static readonly DEBOUNCE_DELAY = ms('5 seconds');
private debounceTimers = new Map<string, NodeJS.Timeout>(); private debounceTimers = new Map<string, NodeJS.Timeout>();
@ -67,10 +69,7 @@ export class AssetProfileChangedListener {
dataSource: DataSource; dataSource: DataSource;
symbol: string; symbol: string;
}) { }) {
Logger.log( this.logger.log(`Asset profile of ${symbol} (${dataSource}) has changed`);
`Asset profile of ${symbol} (${dataSource}) has changed`,
'AssetProfileChangedListener'
);
if ( if (
this.configurationService.get( this.configurationService.get(
@ -84,10 +83,7 @@ export class AssetProfileChangedListener {
const existingCurrencies = this.exchangeRateDataService.getCurrencies(); const existingCurrencies = this.exchangeRateDataService.getCurrencies();
if (!existingCurrencies.includes(currency)) { if (!existingCurrencies.includes(currency)) {
Logger.log( this.logger.log(`New currency ${currency} has been detected`);
`New currency ${currency} has been detected`,
'AssetProfileChangedListener'
);
await this.exchangeRateDataService.initialize(); await this.exchangeRateDataService.initialize();
} }

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

@ -8,6 +8,8 @@ import { PortfolioChangedEvent } from './portfolio-changed.event';
@Injectable() @Injectable()
export class PortfolioChangedListener { export class PortfolioChangedListener {
private readonly logger = new Logger(PortfolioChangedListener.name);
private static readonly DEBOUNCE_DELAY = ms('5 seconds'); private static readonly DEBOUNCE_DELAY = ms('5 seconds');
private debounceTimers = new Map<string, NodeJS.Timeout>(); private debounceTimers = new Map<string, NodeJS.Timeout>();
@ -35,10 +37,7 @@ export class PortfolioChangedListener {
} }
private async processPortfolioChanged({ userId }: { userId: string }) { private async processPortfolioChanged({ userId }: { userId: string }) {
Logger.log( this.logger.log(`Portfolio of user '${userId}' has changed`);
`Portfolio of user '${userId}' has changed`,
'PortfolioChangedListener'
);
await this.redisCacheService.removePortfolioSnapshotsByUserId({ userId }); 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() @Injectable()
export class PerformanceLoggingService { export class PerformanceLoggingService {
private readonly logger = new Logger(PerformanceLoggingService.name);
public logPerformance({ public logPerformance({
className, className,
methodName, methodName,
@ -13,7 +15,7 @@ export class PerformanceLoggingService {
}) { }) {
const endTime = performance.now(); const endTime = performance.now();
Logger.debug( this.logger.debug(
`Completed execution of ${methodName}() in ${((endTime - startTime) / 1000).toFixed(3)} seconds`, `Completed execution of ${methodName}() in ${((endTime - startTime) / 1000).toFixed(3)} seconds`,
className className
); );

18
apps/api/src/main.ts

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

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

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

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

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

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

@ -29,6 +29,8 @@ import { format, fromUnixTime, getUnixTime } from 'date-fns';
@Injectable() @Injectable()
export class CoinGeckoService implements DataProviderInterface, OnModuleInit { export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
private readonly logger = new Logger(CoinGeckoService.name);
private apiUrl: string; private apiUrl: string;
private headers: HeadersInit = {}; private headers: HeadersInit = {};
@ -88,7 +90,7 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'CoinGeckoService'); this.logger.error(message);
} }
return response; return response;
@ -214,7 +216,7 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'CoinGeckoService'); this.logger.error(message);
} }
return response; return response;
@ -262,7 +264,7 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'CoinGeckoService'); this.logger.error(message);
} }
return { items }; 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() @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',
@ -209,9 +211,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return undefined; return undefined;
}) })
.catch(({ message }) => { .catch(({ message }) => {
Logger.error( this.logger.error(
`Failed to search Trackinsight symbol for ${symbol} (${message})`, `Failed to search Trackinsight symbol for ${symbol} (${message})`
'TrackinsightDataEnhancerService'
); );
return undefined; 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() @Injectable()
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
private readonly logger = new Logger(YahooFinanceDataEnhancerService.name);
private readonly yahooFinance = new YahooFinance({ private readonly yahooFinance = new YahooFinance({
suppressNotices: ['yahooSurvey'] suppressNotices: ['yahooSurvey']
}); });
@ -123,7 +125,7 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
response.url = url; response.url = url;
} }
} catch (error) { } catch (error) {
Logger.error(error, 'YahooFinanceDataEnhancerService'); this.logger.error(error);
} }
return response; return response;
@ -266,7 +268,7 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
`No data found, ${aSymbol} (${this.getName()}) may be delisted` `No data found, ${aSymbol} (${this.getName()}) may be delisted`
); );
} else { } 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() @Injectable()
export class DataProviderService implements OnModuleInit { export class DataProviderService implements OnModuleInit {
private readonly logger = new Logger(DataProviderService.name);
private dataProviderMapping: { [dataProviderName: string]: string }; private dataProviderMapping: { [dataProviderName: string]: string };
public constructor( public constructor(
@ -129,7 +131,7 @@ export class DataProviderService implements OnModuleInit {
); );
} }
} catch (error) { } catch (error) {
Logger.error(error, 'DataProviderService'); this.logger.error(error);
throw error; throw error;
} }
@ -391,7 +393,7 @@ export class DataProviderService implements OnModuleInit {
return r; return r;
}, {}); }, {});
} catch (error) { } catch (error) {
Logger.error(error, 'DataProviderService'); this.logger.error(error);
} finally { } finally {
return response; return response;
} }
@ -503,7 +505,7 @@ export class DataProviderService implements OnModuleInit {
result[symbol] = data; result[symbol] = data;
} }
} catch (error) { } catch (error) {
Logger.error(error, 'DataProviderService'); this.logger.error(error);
throw error; throw error;
} }
@ -567,13 +569,12 @@ export class DataProviderService implements OnModuleInit {
const numberOfItemsInCache = Object.keys(response)?.length; const numberOfItemsInCache = Object.keys(response)?.length;
if (numberOfItemsInCache) { if (numberOfItemsInCache) {
Logger.debug( this.logger.debug(
`Fetched ${numberOfItemsInCache} quote${ `Fetched ${numberOfItemsInCache} quote${
numberOfItemsInCache > 1 ? 's' : '' numberOfItemsInCache > 1 ? 's' : ''
} from cache in ${((performance.now() - startTimeTotal) / 1000).toFixed( } from cache in ${((performance.now() - startTimeTotal) / 1000).toFixed(
3 3
)} seconds`, )} seconds`
'DataProviderService'
); );
} }
@ -684,14 +685,13 @@ export class DataProviderService implements OnModuleInit {
} }
} }
Logger.debug( this.logger.debug(
`Fetched ${symbolsChunk.length} quote${ `Fetched ${symbolsChunk.length} quote${
symbolsChunk.length > 1 ? 's' : '' symbolsChunk.length > 1 ? 's' : ''
} from ${dataSource} in ${( } from ${dataSource} in ${(
(performance.now() - startTimeDataSource) / (performance.now() - startTimeDataSource) /
1000 1000
).toFixed(3)} seconds`, ).toFixed(3)} seconds`
'DataProviderService'
); );
try { try {
@ -722,15 +722,18 @@ export class DataProviderService implements OnModuleInit {
await Promise.all(promises); await Promise.all(promises);
Logger.debug('--------------------------------------------------------'); this.logger.debug(
Logger.debug( '--------------------------------------------------------'
);
this.logger.debug(
`Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${( `Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${(
(performance.now() - startTimeTotal) / (performance.now() - startTimeTotal) /
1000 1000
).toFixed(3)} seconds`, ).toFixed(3)} seconds`
'DataProviderService' );
this.logger.debug(
'========================================================'
); );
Logger.debug('========================================================');
return response; 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 export class EodHistoricalDataService
implements DataProviderInterface, OnModuleInit implements DataProviderInterface, OnModuleInit
{ {
private readonly logger = new Logger(EodHistoricalDataService.name);
private apiKey: string; private apiKey: string;
private readonly URL = 'https://eodhistoricaldata.com/api'; private readonly URL = 'https://eodhistoricaldata.com/api';
@ -127,12 +129,11 @@ export class EodHistoricalDataService
return response; return response;
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format( `Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from, from,
DATE_FORMAT DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`, )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
'EodHistoricalDataService'
); );
return {}; return {};
@ -172,9 +173,8 @@ export class EodHistoricalDataService
marketPrice: adjusted_close marketPrice: adjusted_close
}; };
} else { } else {
Logger.error( this.logger.error(
`Could not get historical market data for ${symbol} (${this.getName()}) at ${date}`, `Could not get historical market data for ${symbol} (${this.getName()}) at ${date}`
'EodHistoricalDataService'
); );
} }
@ -292,9 +292,8 @@ export class EodHistoricalDataService
dataSource: this.getName() dataSource: this.getName()
}; };
} else { } else {
Logger.error( this.logger.error(
`Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`, `Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`
'EodHistoricalDataService'
); );
} }
} }
@ -311,7 +310,7 @@ export class EodHistoricalDataService
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'EodHistoricalDataService'); this.logger.error(message);
} }
return {}; return {};
@ -465,7 +464,7 @@ export class EodHistoricalDataService
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'EodHistoricalDataService'); this.logger.error(message);
} }
return searchResult; 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 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',
@ -265,7 +267,7 @@ export class FinancialModelingPrepService
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'FinancialModelingPrepService'); this.logger.error(message);
} }
return response; return response;
@ -325,12 +327,11 @@ export class FinancialModelingPrepService
return response; return response;
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format( `Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from, from,
DATE_FORMAT DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`, )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
'FinancialModelingPrepService'
); );
return {}; return {};
@ -518,7 +519,7 @@ export class FinancialModelingPrepService
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'FinancialModelingPrepService'); this.logger.error(message);
} }
return response; return response;
@ -638,7 +639,7 @@ export class FinancialModelingPrepService
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'FinancialModelingPrepService'); this.logger.error(message);
} }
return { items }; return { items };

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

@ -33,6 +33,8 @@ import { StatusCodes } from 'http-status-codes';
@Injectable() @Injectable()
export class GhostfolioService implements DataProviderInterface { export class GhostfolioService implements DataProviderInterface {
private readonly logger = new Logger(GhostfolioService.name);
private readonly URL = environment.production private readonly URL = environment.production
? 'https://ghostfol.io/api' ? 'https://ghostfol.io/api'
: `${this.configurationService.get('ROOT_URL')}/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.'; '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; 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.'; '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; 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.'; '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( throw new Error(
`Could not get historical market data for ${symbol} (${this.getName()}) from ${format( `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.'; '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; 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.'; '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; 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() @Injectable()
export class GoogleSheetsService implements DataProviderInterface { export class GoogleSheetsService implements DataProviderInterface {
private readonly logger = new Logger(GoogleSheetsService.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
@ -144,7 +146,7 @@ export class GoogleSheetsService implements DataProviderInterface {
return response; return response;
} catch (error) { } catch (error) {
Logger.error(error, 'GoogleSheetsService'); this.logger.error(error);
} }
return {}; return {};

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

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

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

@ -26,6 +26,8 @@ import { format } from 'date-fns';
@Injectable() @Injectable()
export class RapidApiService implements DataProviderInterface { export class RapidApiService implements DataProviderInterface {
private readonly logger = new Logger(RapidApiService.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService private readonly fetchService: FetchService
@ -122,7 +124,7 @@ export class RapidApiService implements DataProviderInterface {
}; };
} }
} catch (error) { } catch (error) {
Logger.error(error, 'RapidApiService'); this.logger.error(error);
} }
return {}; return {};
@ -167,7 +169,7 @@ export class RapidApiService implements DataProviderInterface {
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
Logger.error(message, 'RapidApiService'); this.logger.error(message);
return undefined; 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() @Injectable()
export class YahooFinanceService implements DataProviderInterface { export class YahooFinanceService implements DataProviderInterface {
private readonly logger = new Logger(YahooFinanceService.name);
private readonly yahooFinance = new YahooFinance({ private readonly yahooFinance = new YahooFinance({
suppressNotices: ['yahooSurvey'] suppressNotices: ['yahooSurvey']
}); });
@ -105,12 +107,11 @@ export class YahooFinanceService implements DataProviderInterface {
return response; return response;
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format( `Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from, from,
DATE_FORMAT DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`, )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
'YahooFinanceService'
); );
return {}; return {};
@ -198,12 +199,9 @@ export class YahooFinanceService implements DataProviderInterface {
try { try {
quotes = await this.yahooFinance.quote(yahooFinanceSymbols); quotes = await this.yahooFinance.quote(yahooFinanceSymbols);
} catch (error) { } catch (error) {
Logger.error(error, 'YahooFinanceService'); this.logger.error(error);
Logger.warn( this.logger.warn('Fallback to yahooFinance.quoteSummary()');
'Fallback to yahooFinance.quoteSummary()',
'YahooFinanceService'
);
quotes = await this.getQuotesWithQuoteSummary(yahooFinanceSymbols); quotes = await this.getQuotesWithQuoteSummary(yahooFinanceSymbols);
} }
@ -229,7 +227,7 @@ export class YahooFinanceService implements DataProviderInterface {
return response; return response;
} catch (error) { } catch (error) {
Logger.error(error, 'YahooFinanceService'); this.logger.error(error);
return {}; return {};
} }
@ -334,7 +332,7 @@ export class YahooFinanceService implements DataProviderInterface {
}); });
} }
} catch (error) { } catch (error) {
Logger.error(error, 'YahooFinanceService'); this.logger.error(error);
} }
return { items }; return { items };
@ -365,10 +363,7 @@ export class YahooFinanceService implements DataProviderInterface {
.filter( .filter(
(result): result is PromiseFulfilledResult<QuoteSummaryResult> => { (result): result is PromiseFulfilledResult<QuoteSummaryResult> => {
if (result.status === 'rejected') { if (result.status === 'rejected') {
Logger.error( this.logger.error(`Could not get quote summary: ${result.reason}`);
`Could not get quote summary: ${result.reason}`,
'YahooFinanceService'
);
return false; 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() @Injectable()
export class ExchangeRateDataService { export class ExchangeRateDataService {
private readonly logger = new Logger(ExchangeRateDataService.name);
private currencies: string[] = []; private currencies: string[] = [];
private currencyPairs: DataGatheringItem[] = []; private currencyPairs: DataGatheringItem[] = [];
private derivedCurrencyFactors: { [currencyPair: string]: number } = {}; private derivedCurrencyFactors: { [currencyPair: string]: number } = {};
@ -110,9 +112,8 @@ export class ExchangeRateDataService {
previousExchangeRate; previousExchangeRate;
if (currency === DEFAULT_CURRENCY && isBefore(date, new Date())) { if (currency === DEFAULT_CURRENCY && isBefore(date, new Date())) {
Logger.error( this.logger.error(
`No exchange rate has been found for ${currency}${targetCurrency} at ${dateString}`, `No exchange rate has been found for ${currency}${targetCurrency} at ${dateString}`
'ExchangeRateDataService'
); );
} }
} else { } else {
@ -253,9 +254,8 @@ export class ExchangeRateDataService {
} }
// Fallback with error, if currencies are not available // Fallback with error, if currencies are not available
Logger.error( this.logger.error(
`No exchange rate has been found for ${aFromCurrency}${aToCurrency}`, `No exchange rate has been found for ${aFromCurrency}${aToCurrency}`
'ExchangeRateDataService'
); );
return aValue; return aValue;
@ -341,12 +341,11 @@ export class ExchangeRateDataService {
return factor * aValue; return factor * aValue;
} }
Logger.error( this.logger.error(
`No exchange rate has been found for ${aFromCurrency}${aToCurrency} at ${format( `No exchange rate has been found for ${aFromCurrency}${aToCurrency} at ${format(
aDate, aDate,
DATE_FORMAT DATE_FORMAT
)}`, )}`
'ExchangeRateDataService'
); );
return undefined; return undefined;
@ -483,7 +482,7 @@ export class ExchangeRateDataService {
errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`; 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() @Injectable()
export class FetchService implements OnModuleInit { 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 REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token'];
private static readonly WEB_FETCH_TIMEOUT = ms('30 seconds'); 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 url = input instanceof Request ? input.url : input.toString();
const urlRedacted = this.redactUrl(url); const urlRedacted = this.redactUrl(url);
Logger.debug(`${method} ${urlRedacted}`, 'FetchService'); this.logger.debug(`${method} ${urlRedacted}`);
if (method === 'GET') { if (method === 'GET') {
const webFetchRoute = this.getMatchingWebFetchRoute(url); const webFetchRoute = this.getMatchingWebFetchRoute(url);
@ -60,15 +62,11 @@ export class FetchService implements OnModuleInit {
return await globalThis.fetch(input, init); return await globalThis.fetch(input, init);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
Logger.error( this.logger.error(
`${method} ${urlRedacted} failed: [${error.name}] ${error.message}`, `${method} ${urlRedacted} failed: [${error.name}] ${error.message}`
'FetchService'
); );
} else { } else {
Logger.error( this.logger.error(`${method} ${urlRedacted} failed: ${String(error)}`);
`${method} ${urlRedacted} failed: ${String(error)}`,
'FetchService'
);
} }
throw error; throw error;
@ -145,10 +143,7 @@ export class FetchService implements OnModuleInit {
} }
} }
Logger.debug( this.logger.debug(`Routed ${this.redactUrl(url)} via web fetch tool`);
`Routed ${this.redactUrl(url)} via web fetch tool`,
'FetchService'
);
return new Response(body, { return new Response(body, {
headers: webFetchRoute.responseContentType headers: webFetchRoute.responseContentType
@ -159,11 +154,10 @@ export class FetchService implements OnModuleInit {
return undefined; return undefined;
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Web fetch tool failed for ${this.redactUrl(url)}: ${ `Web fetch tool failed for ${this.redactUrl(url)}: ${
error instanceof Error ? error.message : String(error) error instanceof Error ? error.message : String(error)
}`, }`
'FetchService'
); );
return undefined; return undefined;

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

@ -7,6 +7,8 @@ import { join } from 'node:path';
@Injectable() @Injectable()
export class I18nService implements OnModuleInit { export class I18nService implements OnModuleInit {
private readonly logger = new Logger(I18nService.name);
private localesPath = join(__dirname, 'assets', 'locales'); private localesPath = join(__dirname, 'assets', 'locales');
private translations: { [locale: string]: cheerio.CheerioAPI } = {}; private translations: { [locale: string]: cheerio.CheerioAPI } = {};
@ -26,7 +28,7 @@ export class I18nService implements OnModuleInit {
const $ = this.translations[languageCode]; const $ = this.translations[languageCode];
if (!$) { if (!$) {
Logger.warn(`Translation not found for locale '${languageCode}'`); this.logger.warn(`Translation not found for locale '${languageCode}'`);
} }
let translatedText = $( let translatedText = $(
@ -36,7 +38,7 @@ export class I18nService implements OnModuleInit {
).text(); ).text();
if (!translatedText) { if (!translatedText) {
Logger.warn( this.logger.warn(
`Translation not found for id '${id}' in locale '${languageCode}'` `Translation not found for id '${id}' in locale '${languageCode}'`
); );
} }
@ -60,7 +62,7 @@ export class I18nService implements OnModuleInit {
this.parseXml(xmlData); this.parseXml(xmlData);
} }
} catch (error) { } 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 extends PrismaClient
implements OnModuleInit, OnModuleDestroy implements OnModuleInit, OnModuleDestroy
{ {
private readonly logger = new Logger(PrismaService.name);
public constructor(configService: ConfigService) { public constructor(configService: ConfigService) {
const adapter = new PrismaPg({ const adapter = new PrismaPg({
connectionString: configService.get<string>('DATABASE_URL') connectionString: configService.get<string>('DATABASE_URL')
@ -43,7 +45,7 @@ export class PrismaService
try { try {
await this.$connect(); await this.$connect();
} catch (error) { } 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() @Injectable()
@Processor(DATA_GATHERING_QUEUE) @Processor(DATA_GATHERING_QUEUE)
export class DataGatheringProcessor { export class DataGatheringProcessor {
private readonly logger = new Logger(DataGatheringProcessor.name);
public constructor( public constructor(
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
@ -51,16 +53,14 @@ export class DataGatheringProcessor {
const { dataSource, symbol } = job.data; const { dataSource, symbol } = job.data;
try { try {
Logger.log( this.logger.log(
`Asset profile data gathering has been started for ${symbol} (${dataSource})`, `Asset profile data gathering has been started for ${symbol} (${dataSource})`
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
); );
await this.dataGatheringService.gatherAssetProfiles([job.data]); await this.dataGatheringService.gatherAssetProfiles([job.data]);
Logger.log( this.logger.log(
`Asset profile data gathering has been completed for ${symbol} (${dataSource})`, `Asset profile data gathering has been completed for ${symbol} (${dataSource})`
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
); );
} catch (error) { } catch (error) {
if (error instanceof AssetProfileDelistedError) { if (error instanceof AssetProfileDelistedError) {
@ -74,18 +74,14 @@ export class DataGatheringProcessor {
} }
); );
Logger.log( this.logger.log(
`Asset profile data gathering has been discarded for ${symbol} (${dataSource})`, `Asset profile data gathering has been discarded for ${symbol} (${dataSource})`
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
); );
return job.discard(); return job.discard();
} }
Logger.error( this.logger.error(error);
error,
`DataGatheringProcessor (${GATHER_ASSET_PROFILE_PROCESS_JOB_NAME})`
);
throw error; throw error;
} }
@ -105,12 +101,11 @@ export class DataGatheringProcessor {
try { try {
let currentDate = parseISO(date as unknown as string); 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( `Historical market data gathering has been started for ${symbol} (${dataSource}) at ${format(
currentDate, currentDate,
DATE_FORMAT DATE_FORMAT
)}${force ? ' (forced update)' : ''}`, )}${force ? ' (forced update)' : ''}`
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
); );
const historicalData = await this.dataProviderService.getHistoricalRaw({ const historicalData = await this.dataProviderService.getHistoricalRaw({
@ -167,12 +162,11 @@ export class DataGatheringProcessor {
await this.marketDataService.updateMany({ data }); await this.marketDataService.updateMany({ data });
} }
Logger.log( this.logger.log(
`Historical market data gathering has been completed for ${symbol} (${dataSource}) at ${format( `Historical market data gathering has been completed for ${symbol} (${dataSource}) at ${format(
currentDate, currentDate,
DATE_FORMAT DATE_FORMAT
)}`, )}`
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
); );
} catch (error) { } catch (error) {
if (error instanceof AssetProfileDelistedError) { if (error instanceof AssetProfileDelistedError) {
@ -186,18 +180,14 @@ export class DataGatheringProcessor {
} }
); );
Logger.log( this.logger.log(
`Historical market data gathering has been discarded for ${symbol} (${dataSource})`, `Historical market data gathering has been discarded for ${symbol} (${dataSource})`
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
); );
return job.discard(); return job.discard();
} }
Logger.error( this.logger.error(error);
error,
`DataGatheringProcessor (${GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME})`
);
throw 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() @Injectable()
export class DataGatheringService { export class DataGatheringService {
private readonly logger = new Logger(DataGatheringService.name);
public constructor( public constructor(
@Inject('DataEnhancers') @Inject('DataEnhancers')
private readonly dataEnhancers: DataEnhancerInterface[], private readonly dataEnhancers: DataEnhancerInterface[],
@ -145,7 +147,7 @@ export class DataGatheringService {
}); });
} }
} catch (error) { } catch (error) {
Logger.error(error, 'DataGatheringService'); this.logger.error(error);
} finally { } finally {
return undefined; return undefined;
} }
@ -200,12 +202,11 @@ export class DataGatheringService {
symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol
}); });
} catch (error) { } catch (error) {
Logger.error( this.logger.error(
`Failed to enhance data for ${symbol} (${ `Failed to enhance data for ${symbol} (${
assetProfile.dataSource assetProfile.dataSource
}) by ${dataEnhancer.getName()}`, }) by ${dataEnhancer.getName()}`,
error, error
'DataGatheringService'
); );
} }
} }
@ -269,11 +270,7 @@ export class DataGatheringService {
} }
}); });
} catch (error) { } catch (error) {
Logger.error( this.logger.error(`${symbol}: ${error?.meta?.cause}`, error);
`${symbol}: ${error?.meta?.cause}`,
error,
'DataGatheringService'
);
if (assetProfileIdentifiers.length === 1) { if (assetProfileIdentifiers.length === 1) {
throw error; 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() @Injectable()
@Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE) @Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE)
export class PortfolioSnapshotProcessor { export class PortfolioSnapshotProcessor {
private readonly logger = new Logger(PortfolioSnapshotProcessor.name);
public constructor( public constructor(
private readonly accountBalanceService: AccountBalanceService, private readonly accountBalanceService: AccountBalanceService,
private readonly activitiesService: ActivitiesService, private readonly activitiesService: ActivitiesService,
@ -41,9 +43,8 @@ export class PortfolioSnapshotProcessor {
try { try {
const startTime = performance.now(); const startTime = performance.now();
Logger.log( this.logger.log(
`Portfolio snapshot calculation of user '${job.data.userId}' has been started`, `Portfolio snapshot calculation of user '${job.data.userId}' has been started`
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
); );
const { activities } = const { activities } =
@ -72,12 +73,11 @@ export class PortfolioSnapshotProcessor {
const snapshot = await portfolioCalculator.computeSnapshot(); const snapshot = await portfolioCalculator.computeSnapshot();
Logger.log( this.logger.log(
`Portfolio snapshot calculation of user '${job.data.userId}' has been completed in ${( `Portfolio snapshot calculation of user '${job.data.userId}' has been completed in ${(
(performance.now() - startTime) / (performance.now() - startTime) /
1000 1000
).toFixed(3)} seconds`, ).toFixed(3)} seconds`
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
); );
const expiration = addMilliseconds( const expiration = addMilliseconds(
@ -101,10 +101,7 @@ export class PortfolioSnapshotProcessor {
return snapshot; return snapshot;
} catch (error) { } catch (error) {
Logger.error( this.logger.error(error);
error,
`PortfolioSnapshotProcessor (${PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME})`
);
throw new 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() @Injectable()
@Processor(STATISTICS_GATHERING_QUEUE) @Processor(STATISTICS_GATHERING_QUEUE)
export class StatisticsGatheringProcessor { export class StatisticsGatheringProcessor {
private readonly logger = new Logger(StatisticsGatheringProcessor.name);
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService, private readonly fetchService: FetchService,
@ -35,10 +37,7 @@ export class StatisticsGatheringProcessor {
@Process(GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME) @Process(GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME)
public async gatherDockerHubPullsStatistics() { public async gatherDockerHubPullsStatistics() {
Logger.log( this.logger.log('Docker Hub pulls statistics gathering has been started');
'Docker Hub pulls statistics gathering has been started',
'StatisticsGatheringProcessor'
);
const dockerHubPulls = await this.countDockerHubPulls(); const dockerHubPulls = await this.countDockerHubPulls();
@ -47,17 +46,13 @@ export class StatisticsGatheringProcessor {
value: String(dockerHubPulls) value: String(dockerHubPulls)
}); });
Logger.log( this.logger.log('Docker Hub pulls statistics gathering has been completed');
'Docker Hub pulls statistics gathering has been completed',
'StatisticsGatheringProcessor'
);
} }
@Process(GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME) @Process(GATHER_STATISTICS_GITHUB_CONTRIBUTORS_PROCESS_JOB_NAME)
public async gatherGitHubContributorsStatistics() { public async gatherGitHubContributorsStatistics() {
Logger.log( this.logger.log(
'GitHub contributors statistics gathering has been started', 'GitHub contributors statistics gathering has been started'
'StatisticsGatheringProcessor'
); );
const gitHubContributors = await this.countGitHubContributors(); const gitHubContributors = await this.countGitHubContributors();
@ -67,18 +62,14 @@ export class StatisticsGatheringProcessor {
value: String(gitHubContributors) value: String(gitHubContributors)
}); });
Logger.log( this.logger.log(
'GitHub contributors statistics gathering has been completed', 'GitHub contributors statistics gathering has been completed'
'StatisticsGatheringProcessor'
); );
} }
@Process(GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME) @Process(GATHER_STATISTICS_GITHUB_STARGAZERS_PROCESS_JOB_NAME)
public async gatherGitHubStargazersStatistics() { public async gatherGitHubStargazersStatistics() {
Logger.log( this.logger.log('GitHub stargazers statistics gathering has been started');
'GitHub stargazers statistics gathering has been started',
'StatisticsGatheringProcessor'
);
const gitHubStargazers = await this.countGitHubStargazers(); const gitHubStargazers = await this.countGitHubStargazers();
@ -87,9 +78,8 @@ export class StatisticsGatheringProcessor {
value: String(gitHubStargazers) value: String(gitHubStargazers)
}); });
Logger.log( this.logger.log(
'GitHub stargazers statistics gathering has been completed', 'GitHub stargazers statistics gathering has been completed'
'StatisticsGatheringProcessor'
); );
} }
@ -100,18 +90,14 @@ export class StatisticsGatheringProcessor {
); );
if (!monitorId) { if (!monitorId) {
Logger.log( this.logger.log(
`Uptime statistics gathering has been skipped as no ${PROPERTY_BETTER_UPTIME_MONITOR_ID} is configured`, `Uptime statistics gathering has been skipped as no ${PROPERTY_BETTER_UPTIME_MONITOR_ID} is configured`
'StatisticsGatheringProcessor'
); );
return; return;
} }
Logger.log( this.logger.log('Uptime statistics gathering has been started');
'Uptime statistics gathering has been started',
'StatisticsGatheringProcessor'
);
const uptime = await this.getUptime(monitorId); const uptime = await this.getUptime(monitorId);
@ -120,10 +106,7 @@ export class StatisticsGatheringProcessor {
value: String(uptime) value: String(uptime)
}); });
Logger.log( this.logger.log('Uptime statistics gathering has been completed');
'Uptime statistics gathering has been completed',
'StatisticsGatheringProcessor'
);
} }
private async countDockerHubPulls(): Promise<number> { private async countDockerHubPulls(): Promise<number> {
@ -139,7 +122,7 @@ export class StatisticsGatheringProcessor {
return pull_count; return pull_count;
} catch (error) { } catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - DockerHub'); this.logger.error(error);
throw error; throw error;
} }
@ -169,7 +152,7 @@ export class StatisticsGatheringProcessor {
value value
}); });
} catch (error) { } catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - GitHub'); this.logger.error(error);
throw error; throw error;
} }
@ -188,7 +171,7 @@ export class StatisticsGatheringProcessor {
return stargazers_count; return stargazers_count;
} catch (error) { } catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - GitHub'); this.logger.error(error);
throw error; throw error;
} }
@ -217,7 +200,7 @@ export class StatisticsGatheringProcessor {
return data.attributes.availability / 100; return data.attributes.availability / 100;
} catch (error) { } catch (error) {
Logger.error(error, 'StatisticsGatheringProcessor - Better Stack'); this.logger.error(error);
throw 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() @Injectable()
export class TwitterBotService implements OnModuleInit { export class TwitterBotService implements OnModuleInit {
private readonly logger = new Logger(TwitterBotService.name);
private twitterClient: TwitterApiReadWrite; private twitterClient: TwitterApiReadWrite;
public constructor( public constructor(
@ -71,13 +73,12 @@ export class TwitterBotService implements OnModuleInit {
const { data: createdTweet } = const { data: createdTweet } =
await this.twitterClient.v2.tweet(status); await this.twitterClient.v2.tweet(status);
Logger.log( this.logger.log(
`Fear & Greed Index has been posted: https://x.com/ghostfolio_/status/${createdTweet.id}`, `Fear & Greed Index has been posted: https://x.com/ghostfolio_/status/${createdTweet.id}`
'TwitterBotService'
); );
} }
} catch (error) { } 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>
<trans-unit id="9153520284278555926" datatype="html"> <trans-unit id="9153520284278555926" datatype="html">
<source>please</source> <source>please</source>
<target state="new">please</target> <target state="translated">будь ласка</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context>
<context context-type="linenumber">333</context> <context context-type="linenumber">333</context>
@ -360,7 +360,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1351814922314683865" datatype="html"> <trans-unit id="1351814922314683865" datatype="html">
<source>with</source> <source>with</source>
<target state="new">with</target> <target state="translated">з</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html</context>
<context context-type="linenumber">87</context> <context context-type="linenumber">87</context>
@ -1388,7 +1388,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7702646444963497962" datatype="html"> <trans-unit id="7702646444963497962" datatype="html">
<source>By</source> <source>By</source>
<target state="new">By</target> <target state="translated">До</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">139</context> <context context-type="linenumber">139</context>
@ -1892,7 +1892,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5303806780432428245" datatype="html"> <trans-unit id="5303806780432428245" datatype="html">
<source>Indonesia</source> <source>Indonesia</source>
<target state="new">Indonesia</target> <target state="translated">Індонезія</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">90</context> <context context-type="linenumber">90</context>
@ -2108,7 +2108,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8186013988289067040" datatype="html"> <trans-unit id="8186013988289067040" datatype="html">
<source>Code</source> <source>Code</source>
<target state="new">Code</target> <target state="translated">Код</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">159</context> <context context-type="linenumber">159</context>
@ -2636,7 +2636,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2575998129003872734" datatype="html"> <trans-unit id="2575998129003872734" datatype="html">
<source>Argentina</source> <source>Argentina</source>
<target state="new">Argentina</target> <target state="translated">Аргентина</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">78</context> <context context-type="linenumber">78</context>
@ -3204,7 +3204,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8553460997100418147" datatype="html"> <trans-unit id="8553460997100418147" datatype="html">
<source>for</source> <source>for</source>
<target state="new">for</target> <target state="translated">для</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html</context>
<context context-type="linenumber">128</context> <context context-type="linenumber">128</context>
@ -3560,7 +3560,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7410432243549869948" datatype="html"> <trans-unit id="7410432243549869948" datatype="html">
<source>Duration</source> <source>Duration</source>
<target state="new">Duration</target> <target state="translated">Тривалість</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-overview/admin-overview.html</context>
<context context-type="linenumber">172</context> <context context-type="linenumber">172</context>
@ -4897,7 +4897,7 @@
</trans-unit> </trans-unit>
<trans-unit id="858192247408211331" datatype="html"> <trans-unit id="858192247408211331" datatype="html">
<source>here</source> <source>here</source>
<target state="new">here</target> <target state="translated">тут</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context>
<context context-type="linenumber">347</context> <context context-type="linenumber">347</context>
@ -5113,7 +5113,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6962217007874959362" datatype="html"> <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> <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-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/pricing/pricing-page.html</context>
<context context-type="linenumber">7</context> <context context-type="linenumber">7</context>
@ -6392,7 +6392,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5515771028435710194" datatype="html"> <trans-unit id="5515771028435710194" datatype="html">
<source>Loan</source> <source>Loan</source>
<target state="new">Loan</target> <target state="translated">Позика</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">58</context> <context context-type="linenumber">58</context>
@ -6944,7 +6944,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4145496584631696119" datatype="html"> <trans-unit id="4145496584631696119" datatype="html">
<source>Role</source> <source>Role</source>
<target state="new">Role</target> <target state="translated">Роль</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html</context>
<context context-type="linenumber">39</context> <context context-type="linenumber">39</context>
@ -7080,7 +7080,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8966698274727122602" datatype="html"> <trans-unit id="8966698274727122602" datatype="html">
<source>Authentication</source> <source>Authentication</source>
<target state="new">Authentication</target> <target state="translated">Автентифікація</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html</context> <context context-type="sourcefile">apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html</context>
<context context-type="linenumber">60</context> <context context-type="linenumber">60</context>
@ -7476,7 +7476,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8540986733881734625" datatype="html"> <trans-unit id="8540986733881734625" datatype="html">
<source>Lazy</source> <source>Lazy</source>
<target state="new">Lazy</target> <target state="translated">Лінивий</target>
<context-group purpose="location"> <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="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">235</context> <context context-type="linenumber">235</context>
@ -7484,7 +7484,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6882618704933649036" datatype="html"> <trans-unit id="6882618704933649036" datatype="html">
<source>Instant</source> <source>Instant</source>
<target state="new">Instant</target> <target state="translated">Миттєвий</target>
<context-group purpose="location"> <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="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">239</context> <context context-type="linenumber">239</context>
@ -7500,7 +7500,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1713271461473302108" datatype="html"> <trans-unit id="1713271461473302108" datatype="html">
<source>Mode</source> <source>Mode</source>
<target state="new">Mode</target> <target state="translated">Режим</target>
<context-group purpose="location"> <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="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">519</context> <context context-type="linenumber">519</context>
@ -7508,7 +7508,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3540108566782816830" datatype="html"> <trans-unit id="3540108566782816830" datatype="html">
<source>Selector</source> <source>Selector</source>
<target state="new">Selector</target> <target state="translated">Селектор</target>
<context-group purpose="location"> <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="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">535</context> <context context-type="linenumber">535</context>
@ -7532,7 +7532,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4547068148181074902" datatype="html"> <trans-unit id="4547068148181074902" datatype="html">
<source>real-time</source> <source>real-time</source>
<target state="new">real-time</target> <target state="translated">реальний час</target>
<context-group purpose="location"> <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="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts</context>
<context context-type="linenumber">239</context> <context context-type="linenumber">239</context>
@ -7548,7 +7548,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5674286808255988565" datatype="html"> <trans-unit id="5674286808255988565" datatype="html">
<source>Create</source> <source>Create</source>
<target state="new">Create</target> <target state="translated">Створити</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/tags-selector/tags-selector.component.html</context> <context context-type="sourcefile">libs/ui/src/lib/tags-selector/tags-selector.component.html</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">50</context>
@ -7556,7 +7556,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1230154438678955604" datatype="html"> <trans-unit id="1230154438678955604" datatype="html">
<source>Change</source> <source>Change</source>
<target state="new">Change</target> <target state="translated">Змінити</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/holdings-table/holdings-table.component.html</context> <context context-type="sourcefile">libs/ui/src/lib/holdings-table/holdings-table.component.html</context>
<context context-type="linenumber">138</context> <context context-type="linenumber">138</context>
@ -7568,7 +7568,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1322586333669103999" datatype="html"> <trans-unit id="1322586333669103999" datatype="html">
<source>Performance</source> <source>Performance</source>
<target state="new">Performance</target> <target state="translated">Дохідність</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html</context>
<context context-type="linenumber">6</context> <context context-type="linenumber">6</context>
@ -7624,7 +7624,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5004849258025239958" datatype="html"> <trans-unit id="5004849258025239958" datatype="html">
<source>Armenia</source> <source>Armenia</source>
<target state="new">Armenia</target> <target state="translated">Вірменія</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">77</context> <context context-type="linenumber">77</context>
@ -7640,7 +7640,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4830118002486243553" datatype="html"> <trans-unit id="4830118002486243553" datatype="html">
<source>Singapore</source> <source>Singapore</source>
<target state="new">Singapore</target> <target state="translated">Сінгапур</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">97</context> <context context-type="linenumber">97</context>
@ -7672,7 +7672,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6962699013778688473" datatype="html"> <trans-unit id="6962699013778688473" datatype="html">
<source>Continue</source> <source>Continue</source>
<target state="new">Continue</target> <target state="translated">Продовжити</target>
<context-group purpose="location"> <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="sourcefile">apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html</context>
<context context-type="linenumber">57</context> <context context-type="linenumber">57</context>
@ -7732,7 +7732,7 @@
</trans-unit> </trans-unit>
<trans-unit id="routes.about.termsOfService" datatype="html"> <trans-unit id="routes.about.termsOfService" datatype="html">
<source>terms-of-service</source> <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> <note priority="1" from="description">kebab-case</note>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context> <context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
@ -7781,7 +7781,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4391289919356861627" datatype="html"> <trans-unit id="4391289919356861627" datatype="html">
<source>Apply</source> <source>Apply</source>
<target state="new">Apply</target> <target state="translated">Застосувати</target>
<context-group purpose="location"> <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="sourcefile">apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html</context>
<context context-type="linenumber">154</context> <context context-type="linenumber">154</context>
@ -7837,7 +7837,7 @@
</trans-unit> </trans-unit>
<trans-unit id="322229249598827754" datatype="html"> <trans-unit id="322229249598827754" datatype="html">
<source>someone</source> <source>someone</source>
<target state="new">someone</target> <target state="translated">когось</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.component.ts</context> <context context-type="sourcefile">apps/client/src/app/pages/public/public-page.component.ts</context>
<context context-type="linenumber">62</context> <context context-type="linenumber">62</context>
@ -7853,7 +7853,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4558213855845176930" datatype="html"> <trans-unit id="4558213855845176930" datatype="html">
<source>Watchlist</source> <source>Watchlist</source>
<target state="new">Watchlist</target> <target state="translated">Список спостереження</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/home-watchlist/home-watchlist.html</context> <context context-type="sourcefile">apps/client/src/app/components/home-watchlist/home-watchlist.html</context>
<context context-type="linenumber">4</context> <context context-type="linenumber">4</context>
@ -7897,7 +7897,7 @@
</trans-unit> </trans-unit>
<trans-unit id="routes.about.changelog" datatype="html"> <trans-unit id="routes.about.changelog" datatype="html">
<source>changelog</source> <source>changelog</source>
<target state="new">changelog</target> <target state="translated">zhurnal-zmin</target>
<note priority="1" from="description">kebab-case</note> <note priority="1" from="description">kebab-case</note>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context> <context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
@ -8030,7 +8030,7 @@
</trans-unit> </trans-unit>
<trans-unit id="routes.resources.personalFinanceTools" datatype="html"> <trans-unit id="routes.resources.personalFinanceTools" datatype="html">
<source>personal-finance-tools</source> <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> <note priority="1" from="description">kebab-case</note>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context> <context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
@ -8047,7 +8047,7 @@
</trans-unit> </trans-unit>
<trans-unit id="routes.resources.markets" datatype="html"> <trans-unit id="routes.resources.markets" datatype="html">
<source>markets</source> <source>markets</source>
<target state="new">markets</target> <target state="translated">rynky</target>
<note priority="1" from="description">kebab-case</note> <note priority="1" from="description">kebab-case</note>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context> <context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
@ -8108,7 +8108,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3955868613858648955" datatype="html"> <trans-unit id="3955868613858648955" datatype="html">
<source>Available</source> <source>Available</source>
<target state="new">Available</target> <target state="translated">Доступно</target>
<context-group purpose="location"> <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="sourcefile">apps/client/src/app/components/data-provider-status/data-provider-status.component.html</context>
<context context-type="linenumber">3</context> <context context-type="linenumber">3</context>
@ -8116,7 +8116,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5643561794785412000" datatype="html"> <trans-unit id="5643561794785412000" datatype="html">
<source>Unavailable</source> <source>Unavailable</source>
<target state="new">Unavailable</target> <target state="translated">Недоступно</target>
<context-group purpose="location"> <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="sourcefile">apps/client/src/app/components/data-provider-status/data-provider-status.component.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@ -8132,7 +8132,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7387635272539030076" datatype="html"> <trans-unit id="7387635272539030076" datatype="html">
<source>new</source> <source>new</source>
<target state="new">new</target> <target state="translated">новий</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-settings/admin-settings.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-settings/admin-settings.component.html</context>
<context context-type="linenumber">79</context> <context context-type="linenumber">79</context>
@ -8140,7 +8140,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.accountClusterRiskCurrentInvestment" datatype="html"> <trans-unit id="rule.accountClusterRiskCurrentInvestment" datatype="html">
<source>Investment</source> <source>Investment</source>
<target state="new">Investment</target> <target state="translated">Інвестиція</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">15</context> <context context-type="linenumber">15</context>
@ -8164,7 +8164,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.assetClassClusterRiskEquity" datatype="html"> <trans-unit id="rule.assetClassClusterRiskEquity" datatype="html">
<source>Equity</source> <source>Equity</source>
<target state="new">Equity</target> <target state="translated">Акції</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">41</context> <context context-type="linenumber">41</context>
@ -8252,7 +8252,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.currencyClusterRiskCurrentInvestment" datatype="html"> <trans-unit id="rule.currencyClusterRiskCurrentInvestment" datatype="html">
<source>Investment</source> <source>Investment</source>
<target state="new">Investment</target> <target state="translated">Інвестиція</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">95</context> <context context-type="linenumber">95</context>
@ -8276,7 +8276,7 @@
</trans-unit> </trans-unit>
<trans-unit id="routes.start" datatype="html"> <trans-unit id="routes.start" datatype="html">
<source>start</source> <source>start</source>
<target state="new">start</target> <target state="translated">pochatok</target>
<note priority="1" from="description">kebab-case</note> <note priority="1" from="description">kebab-case</note>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context> <context context-type="sourcefile">libs/common/src/lib/routes/routes.ts</context>
@ -8297,7 +8297,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5193539160604294602" datatype="html"> <trans-unit id="5193539160604294602" datatype="html">
<source>Generate</source> <source>Generate</source>
<target state="new">Generate</target> <target state="translated">Згенерувати</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.html</context> <context context-type="sourcefile">apps/client/src/app/components/user-account-access/user-account-access.html</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">45</context>
@ -8313,7 +8313,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2732382861635368484" datatype="html"> <trans-unit id="2732382861635368484" datatype="html">
<source>Stocks</source> <source>Stocks</source>
<target state="new">Stocks</target> <target state="translated">Акції</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/markets/markets.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/markets/markets.component.ts</context>
<context context-type="linenumber">51</context> <context context-type="linenumber">51</context>
@ -8325,7 +8325,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1419479195323304896" datatype="html"> <trans-unit id="1419479195323304896" datatype="html">
<source>Cryptocurrencies</source> <source>Cryptocurrencies</source>
<target state="new">Cryptocurrencies</target> <target state="translated">Криптовалюти</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/markets/markets.component.ts</context> <context context-type="sourcefile">apps/client/src/app/components/markets/markets.component.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
@ -8361,7 +8361,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5795124554973640871" datatype="html"> <trans-unit id="5795124554973640871" datatype="html">
<source>Collectible</source> <source>Collectible</source>
<target state="new">Collectible</target> <target state="translated">Колекційний предмет</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">55</context> <context context-type="linenumber">55</context>
@ -8421,7 +8421,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.fees.category" datatype="html"> <trans-unit id="rule.fees.category" datatype="html">
<source>Fees</source> <source>Fees</source>
<target state="new">Fees</target> <target state="translated">Комісії</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">161</context> <context context-type="linenumber">161</context>
@ -8429,7 +8429,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.liquidity.category" datatype="html"> <trans-unit id="rule.liquidity.category" datatype="html">
<source>Liquidity</source> <source>Liquidity</source>
<target state="new">Liquidity</target> <target state="translated">Ліквідність</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">70</context> <context context-type="linenumber">70</context>
@ -8565,7 +8565,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.regionalMarketClusterRiskAsiaPacific" datatype="html"> <trans-unit id="rule.regionalMarketClusterRiskAsiaPacific" datatype="html">
<source>Asia-Pacific</source> <source>Asia-Pacific</source>
<target state="new">Asia-Pacific</target> <target state="translated">Азіатсько-Тихоокеанський регіон</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">165</context> <context context-type="linenumber">165</context>
@ -8629,7 +8629,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.regionalMarketClusterRiskEurope" datatype="html"> <trans-unit id="rule.regionalMarketClusterRiskEurope" datatype="html">
<source>Europe</source> <source>Europe</source>
<target state="new">Europe</target> <target state="translated">Європа</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">195</context> <context context-type="linenumber">195</context>
@ -8661,7 +8661,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.regionalMarketClusterRiskJapan" datatype="html"> <trans-unit id="rule.regionalMarketClusterRiskJapan" datatype="html">
<source>Japan</source> <source>Japan</source>
<target state="new">Japan</target> <target state="translated">Японія</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">209</context> <context context-type="linenumber">209</context>

Loading…
Cancel
Save