Browse Source

Task/improve language localization of asset classes in AI prompt (#7729)

* Improve language localization of asset classes

* Update changelog
pull/7713/head
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
4c9ba9af83
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 42
      apps/api/src/app/endpoints/ai/ai.service.ts
  3. 2
      apps/api/src/app/endpoints/mcp/mcp.controller.ts
  4. 10
      apps/api/src/services/i18n/i18n.service.ts
  5. 17
      apps/client/src/app/pages/i18n/i18n-page.html

2
CHANGELOG.md

@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the language localization of the asset classes and asset sub classes in the holdings table of the _Copy AI prompt to clipboard for analysis_ action on the analysis page (experimental)
- Improved the language localization of the asset classes and asset sub classes in the holdings table of the _Copy portfolio data to clipboard for AI prompt_ action on the analysis page (experimental)
- Improved the logging of the `web_fetch` tool in the `FetchService` - Improved the logging of the `web_fetch` tool in the `FetchService`
## 3.61.0 - 2026-08-25 ## 3.61.0 - 2026-08-25

42
apps/api/src/app/endpoints/ai/ai.service.ts

@ -1,5 +1,6 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
PROPERTY_API_KEY_OPENROUTER, PROPERTY_API_KEY_OPENROUTER,
@ -11,6 +12,7 @@ import type { AiPromptMode } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { AssetClass, AssetSubClass } from '@prisma/client';
import { generateText } from 'ai'; import { generateText } from 'ai';
import { format } from 'date-fns'; import { format } from 'date-fns';
import type { ColumnDescriptor } from 'tablemark'; import type { ColumnDescriptor } from 'tablemark';
@ -44,6 +46,7 @@ export class AiService {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly i18nService: I18nService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
private readonly propertyService: PropertyService private readonly propertyService: PropertyService
) {} ) {}
@ -103,6 +106,18 @@ export class AiService {
return { name, align: align ?? 'left' }; return { name, align: align ?? 'left' };
}); });
const assetClassTranslations = this.getEnumTranslations({
languageCode,
id: 'assetClass',
values: Object.values(AssetClass)
});
const assetSubClassTranslations = this.getEnumTranslations({
languageCode,
id: 'assetSubClass',
values: Object.values(AssetSubClass)
});
const holdingsTableRows = Object.values(holdings) const holdingsTableRows = Object.values(holdings)
.sort((a, b) => { .sort((a, b) => {
return b.allocationInPercentage - a.allocationInPercentage; return b.allocationInPercentage - a.allocationInPercentage;
@ -132,11 +147,11 @@ export class AiService {
break; break;
case 'ASSET_CLASS': case 'ASSET_CLASS':
row[name] = assetClass ?? ''; row[name] = assetClassTranslations[assetClass] ?? '';
break; break;
case 'ASSET_SUB_CLASS': case 'ASSET_SUB_CLASS':
row[name] = assetSubClass ?? ''; row[name] = assetSubClassTranslations[assetSubClass] ?? '';
break; break;
case 'CURRENCY': case 'CURRENCY':
@ -202,4 +217,27 @@ export class AiService {
`Provide your answer in the following language: ${languageCode}.` `Provide your answer in the following language: ${languageCode}.`
].join('\n'); ].join('\n');
} }
private getEnumTranslations<T extends string>({
id,
languageCode,
values
}: {
id: string;
languageCode: string;
values: T[];
}) {
return values.reduce(
(translations, value) => {
translations[value] =
this.i18nService.getTranslation({
languageCode,
id: `${id}.${value}`
}) || value;
return translations;
},
{} as Record<T, string>
);
}
} }

2
apps/api/src/app/endpoints/mcp/mcp.controller.ts

@ -32,7 +32,7 @@ export class GhostfolioMcpController {
const prompt = await this.aiService.getPrompt({ const prompt = await this.aiService.getPrompt({
filters, filters,
userId, userId,
languageCode: userSettings.language ?? DEFAULT_LANGUAGE_CODE, languageCode: DEFAULT_LANGUAGE_CODE,
mode: 'portfolio', mode: 'portfolio',
userCurrency: userSettings.baseCurrency userCurrency: userSettings.baseCurrency
}); });

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

@ -25,15 +25,21 @@ export class I18nService implements OnModuleInit {
languageCode: string; languageCode: string;
placeholders?: Record<string, string | number>; placeholders?: Record<string, string | number>;
}): string { }): string {
const $ = this.translations[languageCode]; const languageCodeToUse = this.translations[languageCode]
? languageCode
: DEFAULT_LANGUAGE_CODE;
const $ = this.translations[languageCodeToUse];
if (!$) { if (!$) {
this.logger.warn(`Translation not found for locale '${languageCode}'`); this.logger.warn(`Translation not found for locale '${languageCode}'`);
return '';
} }
let translatedText = $( let translatedText = $(
`trans-unit[id="${id}"] > ${ `trans-unit[id="${id}"] > ${
languageCode === DEFAULT_LANGUAGE_CODE ? 'source' : 'target' languageCodeToUse === DEFAULT_LANGUAGE_CODE ? 'source' : 'target'
}` }`
).text(); ).text();

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

@ -1,6 +1,23 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<ul> <ul>
<li i18n="@@assetClass.ALTERNATIVE_INVESTMENT">Alternative Investment</li>
<li i18n="@@assetClass.COMMODITY">Commodity</li>
<li i18n="@@assetClass.EQUITY">Equity</li>
<li i18n="@@assetClass.FIXED_INCOME">Fixed Income</li>
<li i18n="@@assetClass.LIQUIDITY">Liquidity</li>
<li i18n="@@assetClass.REAL_ESTATE">Real Estate</li>
<li i18n="@@assetSubClass.BOND">Bond</li>
<li i18n="@@assetSubClass.CASH">Cash</li>
<li i18n="@@assetSubClass.COLLECTIBLE">Collectible</li>
<li i18n="@@assetSubClass.COMMODITY">Commodity</li>
<li i18n="@@assetSubClass.CRYPTOCURRENCY">Cryptocurrency</li>
<li i18n="@@assetSubClass.ETF">ETF</li>
<li i18n="@@assetSubClass.LOAN">Loan</li>
<li i18n="@@assetSubClass.MUTUALFUND">Mutual Fund</li>
<li i18n="@@assetSubClass.PRECIOUS_METAL">Precious Metal</li>
<li i18n="@@assetSubClass.PRIVATE_EQUITY">Private Equity</li>
<li i18n="@@assetSubClass.STOCK">Stock</li>
<li i18n="@@metaDescription"> <li i18n="@@metaDescription">
Ghostfolio is a personal finance dashboard to keep track of your net Ghostfolio is a personal finance dashboard to keep track of your net
worth including cash, stocks, ETFs and cryptocurrencies across multiple worth including cash, stocks, ETFs and cryptocurrencies across multiple

Loading…
Cancel
Save