diff --git a/workspaces/boost/.changeset/ai-catalog-translations.md b/workspaces/boost/.changeset/ai-catalog-translations.md new file mode 100644 index 00000000000..df0b1ab9dbd --- /dev/null +++ b/workspaces/boost/.changeset/ai-catalog-translations.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-boost': minor +--- + +Add German, Spanish, French, Italian, and Japanese translation files for the AI Catalog frontend. Register all five locales as lazy imports in the translation resource. Add focused tests for locale key parity, interpolation placeholder preservation, and Playwright locale coverage. diff --git a/workspaces/boost/app-config.yaml b/workspaces/boost/app-config.yaml index c18cdbd14a9..9bf94ba63ee 100644 --- a/workspaces/boost/app-config.yaml +++ b/workspaces/boost/app-config.yaml @@ -8,6 +8,10 @@ app: redirects: - from: / to: /ai-catalog + - api:app/app-language: + config: + defaultLanguage: en + availableLanguages: [en, de, es, fr, it, ja] # Example: disable a built-in filter # - ai-catalog-filter:boost/owner: false diff --git a/workspaces/boost/e2e-tests/boost.AiCatalogPage.test.ts b/workspaces/boost/e2e-tests/boost.AiCatalogPage.test.ts index ce64db12b9d..7c45d5533f4 100644 --- a/workspaces/boost/e2e-tests/boost.AiCatalogPage.test.ts +++ b/workspaces/boost/e2e-tests/boost.AiCatalogPage.test.ts @@ -17,6 +17,9 @@ import { expect, test, type Page, type Route } from '@playwright/test'; import { runAccessibilityTests } from './utils/accessibility'; +import { skipIfLocales } from './utils/localeSkip'; + +const NON_EN = ['de', 'es', 'fr', 'it', 'ja']; /** * Locators from Playwright MCP against the live NFS app. After catalog load @@ -127,6 +130,7 @@ test.describe('Boost AI Catalog', () => { test('renders the AI Catalog heading after guest sign-in', async ({ page, }, testInfo) => { + skipIfLocales(testInfo, NON_EN, 'Functional suite is English-only'); await mockCatalogEntities(page, []); await signInAsGuest(page); @@ -144,7 +148,8 @@ test.describe('Boost AI Catalog', () => { test('shows catalog assets when the catalog API returns items', async ({ page, - }) => { + }, testInfo) => { + skipIfLocales(testInfo, NON_EN, 'Functional suite is English-only'); await mockCatalogEntities(page, [skillEntity]); await signInAsGuest(page); @@ -158,7 +163,8 @@ test.describe('Boost AI Catalog', () => { test('shows the catalog error state when the catalog API fails', async ({ page, - }) => { + }, testInfo) => { + skipIfLocales(testInfo, NON_EN, 'Functional suite is English-only'); await page.route('**/api/catalog/**', route => route.abort()); await signInAsGuest(page); @@ -168,7 +174,8 @@ test.describe('Boost AI Catalog', () => { test('Type filter keeps only matching cards and sets type in the URL', async ({ page, - }) => { + }, testInfo) => { + skipIfLocales(testInfo, NON_EN, 'Functional suite is English-only'); await loadTwoAssetCatalog(page); const filters = page.getByRole('navigation', { name: 'Filters' }); @@ -200,7 +207,8 @@ test.describe('Boost AI Catalog', () => { test('search keeps only matching cards and sets q in the URL', async ({ page, - }) => { + }, testInfo) => { + skipIfLocales(testInfo, NON_EN, 'Functional suite is English-only'); await loadTwoAssetCatalog(page); await page.getByRole('searchbox', { name: 'Search' }).fill('Code Review'); @@ -218,6 +226,7 @@ test.describe('Boost AI Catalog', () => { test('uses a mobile filter drawer on smaller screens', async ({ page, }, testInfo) => { + skipIfLocales(testInfo, NON_EN, 'Functional suite is English-only'); await page.setViewportSize({ width: 768, height: 900 }); await loadTwoAssetCatalog(page); @@ -254,7 +263,8 @@ test.describe('Boost AI Catalog', () => { test('table view lists both assets in the data table and sets view=table', async ({ page, - }) => { + }, testInfo) => { + skipIfLocales(testInfo, NON_EN, 'Functional suite is English-only'); await loadTwoAssetCatalog(page); await page.getByRole('radio', { name: 'Table view' }).click(); @@ -276,7 +286,8 @@ test.describe('Boost AI Catalog', () => { test('empty filtered state clears search and restores both cards', async ({ page, - }) => { + }, testInfo) => { + skipIfLocales(testInfo, NON_EN, 'Functional suite is English-only'); await loadTwoAssetCatalog(page); await page.getByRole('searchbox', { name: 'Search' }).fill('zzznomatch'); diff --git a/workspaces/boost/e2e-tests/boost.translations.test.ts b/workspaces/boost/e2e-tests/boost.translations.test.ts new file mode 100644 index 00000000000..a36cd0fe41d --- /dev/null +++ b/workspaces/boost/e2e-tests/boost.translations.test.ts @@ -0,0 +1,162 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, test, type Page, type Route } from '@playwright/test'; + +import { runAccessibilityTests } from './utils/accessibility'; +import { getTranslations, type BoostMessages } from './utils/translations'; + +const LOCALE_DISPLAY_NAMES: Record = { + en: 'English', + de: 'Deutsch', + es: 'Español', + fr: 'Français', + it: 'Italiano', + ja: '日本語', +}; + +const skillEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'AiResource', + metadata: { + name: 'code-review-skill', + title: 'Code Review Skill', + description: 'Automated code review for common issues.', + namespace: 'default', + uid: 'uid-1', + tags: ['security'], + annotations: { 'rhdh.io/ai-asset-source': 'github' }, + }, + spec: { type: 'skill', lifecycle: 'production', owner: 'team-ai-platform' }, +}; + +function isCatalogEntitiesPath(url: URL): boolean { + return ( + url.pathname.endsWith('/api/catalog/entities') && + !url.pathname.includes('/by-query') + ); +} + +async function mockCatalogEntities(page: Page, items: unknown[]) { + const fulfillItemsWrapper = async (route: Route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items }), + }); + + await page.route('**/api/catalog/entities/by-query**', fulfillItemsWrapper); + await page.route(isCatalogEntitiesPath, async route => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(items), + }); + return; + } + await fulfillItemsWrapper(route); + }); +} + +/** + * Sign in as a guest, switch the app language through Settings, and wait for + * the authenticated app shell to render. + */ +async function signInAndSwitchLocale( + page: Page, + locale: string, +): Promise { + page.on('dialog', dialog => dialog.accept()); + await page.goto('/'); + const enter = page.getByRole('button', { name: 'Enter' }); + const settingsLink = page.getByRole('link', { name: 'Settings' }); + await expect(enter.or(settingsLink).first()).toBeVisible({ + timeout: 30_000, + }); + if (await enter.isVisible()) { + await enter.click(); + await settingsLink.waitFor({ state: 'visible', timeout: 30_000 }); + } + + const baseLocale = locale.split('-')[0]; + if (baseLocale !== 'en') { + await settingsLink.click(); + await page.getByRole('button', { name: 'English' }).click(); + await page + .getByRole('option', { name: LOCALE_DISPLAY_NAMES[baseLocale] }) + .click(); + } +} + +test.describe('Boost AI Catalog translations', () => { + test('renders representative strings in the configured locale', async ({ + page, + }) => { + const currentLocale = await page.evaluate( + () => globalThis.navigator.language, + ); + const baseLocale = currentLocale.split('-')[0]; + const translations: BoostMessages = getTranslations(baseLocale); + + await mockCatalogEntities(page, [skillEntity]); + await signInAndSwitchLocale(page, currentLocale); + + await page.getByRole('link', { name: translations.nav.aiCatalog }).click(); + + // This heading is rendered by the page component so it updates at runtime. + await expect( + page.getByRole('heading', { name: translations.catalog.page.title }), + ).toBeVisible(); + // Verify translated controls from both the filter sidebar and toolbar. + await expect( + page.getByRole('navigation', { + name: translations.catalog.filter.title, + }), + ).toBeVisible(); + await expect( + page.getByRole('searchbox', { + name: translations.catalog.toolbar.search, + }), + ).toBeVisible(); + }); + + test('renders empty state in the configured locale', async ({ + page, + }, testInfo) => { + const currentLocale = await page.evaluate( + () => globalThis.navigator.language, + ); + const baseLocale = currentLocale.split('-')[0]; + const translations: BoostMessages = getTranslations(baseLocale); + + await mockCatalogEntities(page, []); + await signInAndSwitchLocale(page, currentLocale); + + await page.getByRole('link', { name: translations.nav.aiCatalog }).click(); + + await expect( + page.getByText(translations.catalog.empty.title), + ).toBeVisible(); + await expect( + page.getByRole('button', { name: translations.catalog.empty.refresh }), + ).toBeVisible(); + + // Accessibility check on the empty state avoids the known + // color-contrast violation on category badges (RHDHBUGS-3738). + await runAccessibilityTests(page, testInfo); + }); +}); diff --git a/workspaces/boost/e2e-tests/utils/localeSkip.ts b/workspaces/boost/e2e-tests/utils/localeSkip.ts new file mode 100644 index 00000000000..2953d5428d4 --- /dev/null +++ b/workspaces/boost/e2e-tests/utils/localeSkip.ts @@ -0,0 +1,31 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, type TestInfo } from '@playwright/test'; + +/** + * Skips the current test when the project (locale) is in the given list. + * Call at the start of a test so it still runs on other locales. + */ +export function skipIfLocales( + testInfo: TestInfo, + locales: string[], + reason: string, +): void { + if (locales.includes(testInfo.project.name)) { + test.skip(true, reason); + } +} diff --git a/workspaces/boost/e2e-tests/utils/translations.ts b/workspaces/boost/e2e-tests/utils/translations.ts new file mode 100644 index 00000000000..6c378bea045 --- /dev/null +++ b/workspaces/boost/e2e-tests/utils/translations.ts @@ -0,0 +1,63 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// These translation files are not exported by the package, so relative imports are necessary for e2e tests +/* eslint-disable @backstage/no-relative-monorepo-imports */ +import { boostMessages } from '../../plugins/boost/src/translations/ref.js'; +import boostTranslationDe from '../../plugins/boost/src/translations/de.js'; +import boostTranslationEs from '../../plugins/boost/src/translations/es.js'; +import boostTranslationFr from '../../plugins/boost/src/translations/fr.js'; +import boostTranslationIt from '../../plugins/boost/src/translations/it.js'; +import boostTranslationJa from '../../plugins/boost/src/translations/ja.js'; +/* eslint-enable @backstage/no-relative-monorepo-imports */ + +export type BoostMessages = typeof boostMessages; + +function transformFlatMessagesIntoTree( + flatMessages: typeof boostTranslationDe.messages, +) { + const messages = {} as Record; + for (const key of Object.keys(flatMessages)) { + const path = key.split('.'); + let current = messages; + for (let i = 0; i < path.length - 1; i++) { + current[path[i]] = current[path[i]] || {}; + current = current[path[i]] as Record; + } + current[path[path.length - 1]] = + flatMessages[key as keyof typeof flatMessages]; + } + return messages as BoostMessages; +} + +export function getTranslations(locale: string): BoostMessages { + switch (locale) { + case 'en': + return boostMessages; + case 'de': + return transformFlatMessagesIntoTree(boostTranslationDe.messages); + case 'es': + return transformFlatMessagesIntoTree(boostTranslationEs.messages); + case 'fr': + return transformFlatMessagesIntoTree(boostTranslationFr.messages); + case 'it': + return transformFlatMessagesIntoTree(boostTranslationIt.messages); + case 'ja': + return transformFlatMessagesIntoTree(boostTranslationJa.messages); + default: + return boostMessages; + } +} diff --git a/workspaces/boost/openspec/changes/ai-catalog-frontend-translations/tasks.md b/workspaces/boost/openspec/changes/ai-catalog-frontend-translations/tasks.md deleted file mode 100644 index 4d8a8333eb0..00000000000 --- a/workspaces/boost/openspec/changes/ai-catalog-frontend-translations/tasks.md +++ /dev/null @@ -1,13 +0,0 @@ -# Tasks: AI Catalog frontend translations (RHIDP-15479) - -- [x] 1. Translation module auto-discovery entry (`./translations`) already exists -- [ ] 2. Create `src/translations/de.ts` -- [ ] 3. Create `src/translations/es.ts` -- [ ] 4. Create `src/translations/fr.ts` -- [ ] 5. Create `src/translations/it.ts` -- [ ] 6. Create `src/translations/ja.ts` -- [ ] 7. Register lazy locale imports in `src/translations/index.ts` -- [ ] 8. Audit user-facing strings against `ref.ts` -- [ ] 9. Preserve interpolation placeholders in all locale files -- [ ] 10. Verify locale switching in the dev app -- [ ] 11. Verify English fallback for missing keys diff --git a/workspaces/boost/openspec/changes/ai-catalog-frontend-translations/.openspec.yaml b/workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/.openspec.yaml similarity index 100% rename from workspaces/boost/openspec/changes/ai-catalog-frontend-translations/.openspec.yaml rename to workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/.openspec.yaml diff --git a/workspaces/boost/openspec/changes/ai-catalog-frontend-translations/design.md b/workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/design.md similarity index 100% rename from workspaces/boost/openspec/changes/ai-catalog-frontend-translations/design.md rename to workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/design.md diff --git a/workspaces/boost/openspec/changes/ai-catalog-frontend-translations/proposal.md b/workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/proposal.md similarity index 100% rename from workspaces/boost/openspec/changes/ai-catalog-frontend-translations/proposal.md rename to workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/proposal.md diff --git a/workspaces/boost/openspec/changes/ai-catalog-frontend-translations/specs/ai-catalog-translations/spec.md b/workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/specs/ai-catalog-translations/spec.md similarity index 100% rename from workspaces/boost/openspec/changes/ai-catalog-frontend-translations/specs/ai-catalog-translations/spec.md rename to workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/specs/ai-catalog-translations/spec.md diff --git a/workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/tasks.md b/workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/tasks.md new file mode 100644 index 00000000000..6166ab51e4d --- /dev/null +++ b/workspaces/boost/openspec/changes/archive/2026-09-17-ai-catalog-frontend-translations/tasks.md @@ -0,0 +1,13 @@ +# Tasks: AI Catalog frontend translations (RHIDP-15479) + +- [x] 1. Translation module auto-discovery entry (`./translations`) already exists +- [x] 2. Create `src/translations/de.ts` +- [x] 3. Create `src/translations/es.ts` +- [x] 4. Create `src/translations/fr.ts` +- [x] 5. Create `src/translations/it.ts` +- [x] 6. Create `src/translations/ja.ts` +- [x] 7. Register lazy locale imports in `src/translations/index.ts` +- [x] 8. Audit user-facing strings against `ref.ts` +- [x] 9. Preserve interpolation placeholders in all locale files +- [ ] 10. ~~Verify locale switching in the dev app~~ — deferred: requires a live RHDH instance with the Settings language selector +- [ ] 11. ~~Verify English fallback for missing keys~~ — deferred: requires a live RHDH instance; Backstage's `createTranslationRef` provides English fallback by design diff --git a/workspaces/boost/openspec/specs/ai-catalog-translations/spec.md b/workspaces/boost/openspec/specs/ai-catalog-translations/spec.md new file mode 100644 index 00000000000..dd105e6ed68 --- /dev/null +++ b/workspaces/boost/openspec/specs/ai-catalog-translations/spec.md @@ -0,0 +1,96 @@ +# ai-catalog-translations Specification + +## Purpose + +Provide German, Spanish, French, Italian, and Japanese translations for the AI Catalog frontend plugin, ensuring complete string coverage and placeholder preservation across all supported RHDH locales. + +## Requirements + +### Requirement: Translation Files for Supported Languages + +Each supported language MUST have a complete translation file following the standard rhdh-plugins pattern. + +#### Scenario: Translation files exist for all 5 languages + +- **GIVEN** the plugin has English strings in `src/translations/ref.ts` +- **WHEN** the translation story is complete +- **THEN** `src/translations/` contains `de.ts`, `es.ts`, `fr.ts`, `it.ts`, `ja.ts` +- **AND** each file uses `createTranslationMessages` referencing the `boostTranslationRef` + +#### Scenario: Translation resource registers all locales + +- **GIVEN** all 5 locale files exist +- **WHEN** the `createTranslationResource` in `src/translations/index.ts` is configured +- **THEN** it lazy-imports all 5 locales (`de: () => import('./de')`, etc.) +- **AND** each locale is only loaded when the user selects that language + +#### Scenario: Translation module auto-discovery for dynamic plugins + +- **GIVEN** the plugin is deployed as a dynamic plugin in RHDH +- **WHEN** the translation module needs to be auto-discovered +- **THEN** the `./translations` package export exposes `boostTranslationsModule` as the default export +- **AND** RHDH auto-discovers the module without explicit `features` array registration + +### Requirement: Complete String Coverage + +Every user-facing string in the plugin MUST be translated. + +#### Scenario: All browse page strings translated + +- **GIVEN** a user switches their RHDH locale to German +- **WHEN** they navigate to `/ai-catalog` +- **THEN** the page title, search placeholder, filter labels, empty state message, error state message, pagination labels, and sort options are all in German + +#### Scenario: All filter strings translated + +- **GIVEN** a user is viewing the AI Catalog in Japanese +- **WHEN** the filter sidebar is visible +- **THEN** filter section headings (type, provider, owner, tags), clear-filters action text, and "has active filters" indicators are in Japanese + +#### Scenario: All entity extension strings translated + +- **GIVEN** a user views an AI asset entity page in French +- **WHEN** entity cards from the boost plugin render +- **THEN** AI asset details, agent instructions, and Usage card titles and actions are all in French + +#### Scenario: Error and empty state strings translated + +- **GIVEN** a user is viewing the AI Catalog in Spanish +- **WHEN** no assets match the current filters +- **THEN** the empty state message and clear-filters button text are in Spanish +- **WHEN** the catalog API is unreachable +- **THEN** the error message and retry button text are in Spanish + +### Requirement: Translation Quality + +Translations MUST be accurate and consistent with RHDH conventions. + +#### Scenario: Translation keys use dot-notation + +- **GIVEN** the English source in `ref.ts` uses nested objects for message keys +- **WHEN** locale files are created +- **THEN** they use flattened dot-notation keys (e.g., `'catalog.filter.type': 'Typ'`) +- **AND** keys match the structure in `ref.ts` exactly + +#### Scenario: Placeholder interpolation preserved + +- **GIVEN** an English string contains interpolation placeholders (e.g., `{{count}} assets`) +- **WHEN** the string is translated +- **THEN** the same placeholders appear in the translated string +- **AND** the interpolation works correctly at runtime + +### Requirement: Verification + +Translations MUST render correctly in the dev app. + +#### Scenario: Locale switching in dev app + +- **GIVEN** the dev app is running +- **WHEN** a developer switches the locale via RHDH Settings +- **THEN** all AI Catalog strings update to the selected language without a page reload + +#### Scenario: Fallback to English for missing keys + +- **GIVEN** a translation file is missing a key that exists in `ref.ts` +- **WHEN** the UI renders that string +- **THEN** the English fallback is shown (not a raw key or empty string) diff --git a/workspaces/boost/packages/app/src/App.tsx b/workspaces/boost/packages/app/src/App.tsx index ba9e6e2b528..6ee0e4307a6 100644 --- a/workspaces/boost/packages/app/src/App.tsx +++ b/workspaces/boost/packages/app/src/App.tsx @@ -15,11 +15,16 @@ */ import { createApp } from '@backstage/frontend-defaults'; - +import { boostTranslationsModule } from '@red-hat-developer-hub/backstage-plugin-boost/translations'; import { navModule } from './modules/nav'; import { sampleFilterModule } from './modules/sampleFilter'; import { signInModule } from './modules/signIn'; export default createApp({ - features: [signInModule, navModule, sampleFilterModule], + features: [ + signInModule, + navModule, + sampleFilterModule, + boostTranslationsModule, + ], }); diff --git a/workspaces/boost/packages/app/src/modules/nav/Sidebar.tsx b/workspaces/boost/packages/app/src/modules/nav/Sidebar.tsx index 7bc2c68f375..a69bec31829 100644 --- a/workspaces/boost/packages/app/src/modules/nav/Sidebar.tsx +++ b/workspaces/boost/packages/app/src/modules/nav/Sidebar.tsx @@ -22,16 +22,19 @@ import { SidebarScrollWrapper, SidebarSpace, } from '@backstage/core-components'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { NavContentBlueprint } from '@backstage/plugin-app-react'; import { UserSettingsSignInAvatar } from '@backstage/plugin-user-settings'; import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; import MenuIcon from '@mui/icons-material/Menu'; +import { boostTranslationRef } from '@red-hat-developer-hub/backstage-plugin-boost'; import { SidebarLogo } from './SidebarLogo'; export const SidebarContent = NavContentBlueprint.make({ params: { - component: ({ navItems }) => { + component: function SidebarNavigation({ navItems }) { + const { t } = useTranslationRef(boostTranslationRef); const nav = navItems.withComponent(item => ( item.icon} to={item.href} text={item.title} /> )); @@ -44,7 +47,7 @@ export const SidebarContent = NavContentBlueprint.make({ {nav.take('page:catalog')} diff --git a/workspaces/boost/playwright.config.ts b/workspaces/boost/playwright.config.ts index eb6f1188df7..5e022702166 100644 --- a/workspaces/boost/playwright.config.ts +++ b/workspaces/boost/playwright.config.ts @@ -16,9 +16,8 @@ import { defineConfig } from '@playwright/test'; -// Boost is NFS-only (no legacy app), so APP_MODE is not used. English-only -// until per-locale test_yamls land (RHIDP-15480 follow-up). -const LOCALES = ['en'] as const; +// Boost is NFS-only (no legacy app), so APP_MODE is not used. +const LOCALES = ['en', 'de', 'es', 'fr', 'it', 'ja'] as const; export default defineConfig({ timeout: 2 * 60 * 1000, diff --git a/workspaces/boost/plugins/boost-entity-provider-sdk/report.api.md b/workspaces/boost/plugins/boost-entity-provider-sdk/report.api.md index b0207feb177..b50035124f8 100644 --- a/workspaces/boost/plugins/boost-entity-provider-sdk/report.api.md +++ b/workspaces/boost/plugins/boost-entity-provider-sdk/report.api.md @@ -194,8 +194,8 @@ export const SkillBundleMetadataSchema: z.ZodObject< { name: string; version: string; - tags?: string[] | undefined; description?: string | undefined; + tags?: string[] | undefined; author?: string | undefined; runtime?: | { @@ -212,8 +212,8 @@ export const SkillBundleMetadataSchema: z.ZodObject< { name: string; version: string; - tags?: string[] | undefined; description?: string | undefined; + tags?: string[] | undefined; author?: string | undefined; runtime?: | { diff --git a/workspaces/boost/plugins/boost/report-translations.api.md b/workspaces/boost/plugins/boost/report-translations.api.md index 38f5cb32504..3c25290a1fd 100644 --- a/workspaces/boost/plugins/boost/report-translations.api.md +++ b/workspaces/boost/plugins/boost/report-translations.api.md @@ -11,12 +11,12 @@ import { TranslationResource } from '@backstage/frontend-plugin-api'; export const boostTranslationRef: TranslationRef< 'plugin.boost', { - readonly 'nav.aiCatalog': string; - readonly 'catalog.table.name': string; - readonly 'catalog.table.type': string; - readonly 'catalog.table.provider': string; - readonly 'catalog.table.owner': string; - readonly 'catalog.table.description': string; + readonly 'catalog.page.title': string; + readonly 'catalog.toolbar.allPrefix': string; + readonly 'catalog.toolbar.search': string; + readonly 'catalog.toolbar.viewGrid': string; + readonly 'catalog.toolbar.viewTable': string; + readonly 'catalog.toolbar.filters': string; readonly 'catalog.filter.title': string; readonly 'catalog.filter.all': string; readonly 'catalog.filter.type': string; @@ -24,17 +24,6 @@ export const boostTranslationRef: TranslationRef< readonly 'catalog.filter.owner': string; readonly 'catalog.filter.tag': string; readonly 'catalog.filter.clearAll': string; - readonly 'catalog.page.title': string; - readonly 'catalog.error.title': string; - readonly 'catalog.error.description': string; - readonly 'catalog.error.retry': string; - readonly 'catalog.toolbar.search': string; - readonly 'catalog.toolbar.filters': string; - readonly 'catalog.toolbar.allPrefix': string; - readonly 'catalog.toolbar.viewGrid': string; - readonly 'catalog.toolbar.viewTable': string; - readonly 'catalog.card.yes': string; - readonly 'catalog.card.no': string; readonly 'catalog.card.assetDetailsTitle': string; readonly 'catalog.card.descriptionLabel': string; readonly 'catalog.card.viewDetails': string; @@ -65,6 +54,13 @@ export const boostTranslationRef: TranslationRef< readonly 'catalog.card.handoffDescriptionTitle': string; readonly 'catalog.card.handoffTargetsTitle': string; readonly 'catalog.card.ragEnabledLabel': string; + readonly 'catalog.card.yes': string; + readonly 'catalog.card.no': string; + readonly 'catalog.table.type': string; + readonly 'catalog.table.provider': string; + readonly 'catalog.table.owner': string; + readonly 'catalog.table.name': string; + readonly 'catalog.table.description': string; readonly 'catalog.empty.title': string; readonly 'catalog.empty.description': string; readonly 'catalog.empty.refresh': string; @@ -72,6 +68,10 @@ export const boostTranslationRef: TranslationRef< readonly 'catalog.emptyFiltered.title': string; readonly 'catalog.emptyFiltered.description': string; readonly 'catalog.emptyFiltered.clearFilters': string; + readonly 'catalog.error.title': string; + readonly 'catalog.error.description': string; + readonly 'catalog.error.retry': string; + readonly 'nav.aiCatalog': string; } >; diff --git a/workspaces/boost/plugins/boost/report.api.md b/workspaces/boost/plugins/boost/report.api.md index f228d6d33d4..9c5d0901d25 100644 --- a/workspaces/boost/plugins/boost/report.api.md +++ b/workspaces/boost/plugins/boost/report.api.md @@ -369,12 +369,12 @@ export default boostPlugin; export const boostTranslationRef: TranslationRef< 'plugin.boost', { - readonly 'nav.aiCatalog': string; - readonly 'catalog.table.name': string; - readonly 'catalog.table.type': string; - readonly 'catalog.table.provider': string; - readonly 'catalog.table.owner': string; - readonly 'catalog.table.description': string; + readonly 'catalog.page.title': string; + readonly 'catalog.toolbar.allPrefix': string; + readonly 'catalog.toolbar.search': string; + readonly 'catalog.toolbar.viewGrid': string; + readonly 'catalog.toolbar.viewTable': string; + readonly 'catalog.toolbar.filters': string; readonly 'catalog.filter.title': string; readonly 'catalog.filter.all': string; readonly 'catalog.filter.type': string; @@ -382,17 +382,6 @@ export const boostTranslationRef: TranslationRef< readonly 'catalog.filter.owner': string; readonly 'catalog.filter.tag': string; readonly 'catalog.filter.clearAll': string; - readonly 'catalog.page.title': string; - readonly 'catalog.error.title': string; - readonly 'catalog.error.description': string; - readonly 'catalog.error.retry': string; - readonly 'catalog.toolbar.search': string; - readonly 'catalog.toolbar.filters': string; - readonly 'catalog.toolbar.allPrefix': string; - readonly 'catalog.toolbar.viewGrid': string; - readonly 'catalog.toolbar.viewTable': string; - readonly 'catalog.card.yes': string; - readonly 'catalog.card.no': string; readonly 'catalog.card.assetDetailsTitle': string; readonly 'catalog.card.descriptionLabel': string; readonly 'catalog.card.viewDetails': string; @@ -423,6 +412,13 @@ export const boostTranslationRef: TranslationRef< readonly 'catalog.card.handoffDescriptionTitle': string; readonly 'catalog.card.handoffTargetsTitle': string; readonly 'catalog.card.ragEnabledLabel': string; + readonly 'catalog.card.yes': string; + readonly 'catalog.card.no': string; + readonly 'catalog.table.type': string; + readonly 'catalog.table.provider': string; + readonly 'catalog.table.owner': string; + readonly 'catalog.table.name': string; + readonly 'catalog.table.description': string; readonly 'catalog.empty.title': string; readonly 'catalog.empty.description': string; readonly 'catalog.empty.refresh': string; @@ -430,6 +426,10 @@ export const boostTranslationRef: TranslationRef< readonly 'catalog.emptyFiltered.title': string; readonly 'catalog.emptyFiltered.description': string; readonly 'catalog.emptyFiltered.clearFilters': string; + readonly 'catalog.error.title': string; + readonly 'catalog.error.description': string; + readonly 'catalog.error.retry': string; + readonly 'nav.aiCatalog': string; } >; diff --git a/workspaces/boost/plugins/boost/src/extensions/aiCatalogPage.tsx b/workspaces/boost/plugins/boost/src/extensions/aiCatalogPage.tsx index 73e11f6cac1..541ab8564e9 100644 --- a/workspaces/boost/plugins/boost/src/extensions/aiCatalogPage.tsx +++ b/workspaces/boost/plugins/boost/src/extensions/aiCatalogPage.tsx @@ -46,6 +46,7 @@ export const aiCatalogPage = PageBlueprint.makeWithOverrides({ path: '/ai-catalog', routeRef: rootRouteRef, title: 'AI Catalog', + noHeader: true, loader: () => import('../pages/AiCatalogPage').then(m => ( diff --git a/workspaces/boost/plugins/boost/src/pages/AiCatalogPage.tsx b/workspaces/boost/plugins/boost/src/pages/AiCatalogPage.tsx index baa9884d33f..d8bf33a10b8 100644 --- a/workspaces/boost/plugins/boost/src/pages/AiCatalogPage.tsx +++ b/workspaces/boost/plugins/boost/src/pages/AiCatalogPage.tsx @@ -15,6 +15,7 @@ */ import { useEffect, useMemo, useState, type ReactNode } from 'react'; +import { Header } from '@backstage/core-components'; import type { SortDescriptor } from '@backstage/ui'; import { CatalogErrorBoundary } from '../components/catalog/CatalogErrorBoundary'; @@ -178,6 +179,7 @@ export const AiCatalogPage = ({ filters }: AiCatalogPageProps) => { title={t('catalog.error.title')} retryLabel={t('catalog.error.retry')} > +
); diff --git a/workspaces/boost/plugins/boost/src/translations/de.ts b/workspaces/boost/plugins/boost/src/translations/de.ts new file mode 100644 index 00000000000..1fa297a201a --- /dev/null +++ b/workspaces/boost/plugins/boost/src/translations/de.ts @@ -0,0 +1,95 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; +import { boostTranslationRef } from './ref'; + +/** + * de translation for plugin.boost. + * @public + */ +const boostTranslationDe = createTranslationMessages({ + ref: boostTranslationRef, + messages: { + 'catalog.page.title': 'KI-Katalog', + 'catalog.toolbar.allPrefix': 'Alle', + 'catalog.toolbar.search': 'Suche', + 'catalog.toolbar.viewGrid': 'Kartenansicht', + 'catalog.toolbar.viewTable': 'Tabellenansicht', + 'catalog.toolbar.filters': 'Filter', + 'catalog.filter.title': 'Filter', + 'catalog.filter.all': 'Alle', + 'catalog.filter.type': 'Typ', + 'catalog.filter.provider': 'Anbieter', + 'catalog.filter.owner': 'Eigentümer', + 'catalog.filter.tag': 'Tag', + 'catalog.filter.clearAll': 'Alle löschen', + 'catalog.card.assetDetailsTitle': 'KI-Asset-Details', + 'catalog.card.descriptionLabel': 'Beschreibung', + 'catalog.card.viewDetails': 'Details zu {{title}} anzeigen', + 'catalog.card.tagsLabel': 'Tags', + 'catalog.card.providerLabel': 'Anbieter', + 'catalog.card.usageTitle': 'Nutzung', + 'catalog.card.versionLabel': 'Version', + 'catalog.card.usageDownloadZip': 'ZIP herunterladen', + 'catalog.card.usageViewSource': 'Quelle anzeigen', + 'catalog.card.serverTypeLabel': 'Servertyp', + 'catalog.card.apiKeyLabel': 'API-Schlüssel erforderlich', + 'catalog.card.defaultModelLabel': 'Standardmodell', + 'catalog.card.rationaleLabel': 'Begründung', + 'catalog.card.disciplinesLabel': 'Disziplinen', + 'catalog.card.categoriesLabel': 'Kategorien', + 'catalog.card.relatedAgentsLabel': 'Verwandte Agenten', + 'catalog.card.ruleCategoryLabel': 'Regelkategorie', + 'catalog.card.toolsLabel': 'Werkzeuge', + 'catalog.card.remotesLabel': 'Remote-Endpunkte', + 'catalog.card.definitionLabel': 'Definition', + 'catalog.card.modelsTitle': 'Modelle', + 'catalog.card.modelTitle': 'Modell', + 'catalog.card.viewModels': 'Alle Modelle anzeigen', + 'catalog.card.modelsDialogTitle': 'Verfügbare Modelle', + 'catalog.card.modelSearch': 'Modelle suchen', + 'catalog.card.noModelsMatch': + 'Keine Modelle stimmen mit Ihrer Suche überein.', + 'catalog.card.instructionsTitle': 'Agentenanweisungen', + 'catalog.card.handoffDescriptionTitle': 'Übergabebeschreibung', + 'catalog.card.handoffTargetsTitle': 'Übergabeziele', + 'catalog.card.ragEnabledLabel': 'RAG aktiviert', + 'catalog.card.yes': 'Ja', + 'catalog.card.no': 'Nein', + 'catalog.table.name': 'Name', + 'catalog.table.type': 'Typ', + 'catalog.table.owner': 'Eigentümer', + 'catalog.table.provider': 'Anbieter', + 'catalog.table.description': 'Beschreibung', + 'catalog.empty.title': 'Keine KI-Assets verfügbar', + 'catalog.empty.description': + 'KI-Assets erscheinen hier, nachdem sie veröffentlicht oder aus Ihrem Katalog synchronisiert wurden. Beim ersten Laden kann dies einen Moment dauern.', + 'catalog.empty.refresh': 'Aktualisieren', + 'catalog.empty.learnMore': 'Veröffentlichung erlernen', + 'catalog.emptyFiltered.title': 'Keine KI-Assets entsprechen Ihren Filtern', + 'catalog.emptyFiltered.description': + 'Versuchen Sie, Ihre Such- oder Filterkriterien anzupassen, um zu finden, wonach Sie suchen.', + 'catalog.emptyFiltered.clearFilters': 'Filter löschen', + 'catalog.error.title': 'KI-Assets konnten nicht geladen werden', + 'catalog.error.description': + 'Beim Verbinden mit dem Katalog ist ein Problem aufgetreten. Überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut.', + 'catalog.error.retry': 'Erneut versuchen', + 'nav.aiCatalog': 'KI-Katalog', + }, +}); + +export default boostTranslationDe; diff --git a/workspaces/boost/plugins/boost/src/translations/es.ts b/workspaces/boost/plugins/boost/src/translations/es.ts new file mode 100644 index 00000000000..7c9bae2360f --- /dev/null +++ b/workspaces/boost/plugins/boost/src/translations/es.ts @@ -0,0 +1,95 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; +import { boostTranslationRef } from './ref'; + +/** + * es translation for plugin.boost. + * @public + */ +const boostTranslationEs = createTranslationMessages({ + ref: boostTranslationRef, + messages: { + 'catalog.page.title': 'Catálogo de IA', + 'catalog.toolbar.allPrefix': 'Todos', + 'catalog.toolbar.search': 'Buscar', + 'catalog.toolbar.viewGrid': 'Vista de tarjetas', + 'catalog.toolbar.viewTable': 'Vista de tabla', + 'catalog.toolbar.filters': 'Filtros', + 'catalog.filter.title': 'Filtros', + 'catalog.filter.all': 'Todos', + 'catalog.filter.type': 'Tipo', + 'catalog.filter.provider': 'Proveedor', + 'catalog.filter.owner': 'Propietario', + 'catalog.filter.tag': 'Etiqueta', + 'catalog.filter.clearAll': 'Borrar todo', + 'catalog.card.assetDetailsTitle': 'Detalles del recurso de IA', + 'catalog.card.descriptionLabel': 'Descripción', + 'catalog.card.viewDetails': 'Ver detalles de {{title}}', + 'catalog.card.tagsLabel': 'Etiquetas', + 'catalog.card.providerLabel': 'Proveedor', + 'catalog.card.usageTitle': 'Uso', + 'catalog.card.versionLabel': 'Versión', + 'catalog.card.usageDownloadZip': 'Descargar ZIP', + 'catalog.card.usageViewSource': 'Ver código fuente', + 'catalog.card.serverTypeLabel': 'Tipo de servidor', + 'catalog.card.apiKeyLabel': 'Clave de API requerida', + 'catalog.card.defaultModelLabel': 'Modelo predeterminado', + 'catalog.card.rationaleLabel': 'Justificación', + 'catalog.card.disciplinesLabel': 'Disciplinas', + 'catalog.card.categoriesLabel': 'Categorías', + 'catalog.card.relatedAgentsLabel': 'Agentes relacionados', + 'catalog.card.ruleCategoryLabel': 'Categoría de regla', + 'catalog.card.toolsLabel': 'Herramientas', + 'catalog.card.remotesLabel': 'Puntos de acceso remotos', + 'catalog.card.definitionLabel': 'Definición', + 'catalog.card.modelsTitle': 'Modelos', + 'catalog.card.modelTitle': 'Modelo', + 'catalog.card.viewModels': 'Ver todos los modelos', + 'catalog.card.modelsDialogTitle': 'Modelos disponibles', + 'catalog.card.modelSearch': 'Buscar modelos', + 'catalog.card.noModelsMatch': 'Ningún modelo coincide con su búsqueda.', + 'catalog.card.instructionsTitle': 'Instrucciones del agente', + 'catalog.card.handoffDescriptionTitle': 'Descripción de transferencia', + 'catalog.card.handoffTargetsTitle': 'Destinos de transferencia', + 'catalog.card.ragEnabledLabel': 'RAG habilitado', + 'catalog.card.yes': 'Sí', + 'catalog.card.no': 'No', + 'catalog.table.name': 'Nombre', + 'catalog.table.type': 'Tipo', + 'catalog.table.owner': 'Propietario', + 'catalog.table.provider': 'Proveedor', + 'catalog.table.description': 'Descripción', + 'catalog.empty.title': 'No hay recursos de IA disponibles', + 'catalog.empty.description': + 'Los recursos de IA aparecen aquí después de ser publicados o sincronizados desde su catálogo. En la primera carga, esto puede tardar un momento.', + 'catalog.empty.refresh': 'Actualizar', + 'catalog.empty.learnMore': 'Cómo publicar', + 'catalog.emptyFiltered.title': + 'Ningún recurso de IA coincide con sus filtros', + 'catalog.emptyFiltered.description': + 'Intente ajustar sus criterios de búsqueda o filtro para encontrar lo que busca.', + 'catalog.emptyFiltered.clearFilters': 'Borrar filtros', + 'catalog.error.title': 'Error al cargar los recursos de IA', + 'catalog.error.description': + 'Hubo un problema al conectar con el catálogo. Verifique su conexión de red e intente nuevamente.', + 'catalog.error.retry': 'Reintentar', + 'nav.aiCatalog': 'Catálogo de IA', + }, +}); + +export default boostTranslationEs; diff --git a/workspaces/boost/plugins/boost/src/translations/fr.ts b/workspaces/boost/plugins/boost/src/translations/fr.ts new file mode 100644 index 00000000000..7023a00714c --- /dev/null +++ b/workspaces/boost/plugins/boost/src/translations/fr.ts @@ -0,0 +1,96 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; +import { boostTranslationRef } from './ref'; + +/** + * fr translation for plugin.boost. + * @public + */ +const boostTranslationFr = createTranslationMessages({ + ref: boostTranslationRef, + messages: { + 'catalog.page.title': 'Catalogue IA', + 'catalog.toolbar.allPrefix': 'Tous', + 'catalog.toolbar.search': 'Rechercher', + 'catalog.toolbar.viewGrid': 'Vue en cartes', + 'catalog.toolbar.viewTable': 'Vue en tableau', + 'catalog.toolbar.filters': 'Filtres', + 'catalog.filter.title': 'Filtres', + 'catalog.filter.all': 'Tous', + 'catalog.filter.type': 'Type', + 'catalog.filter.provider': 'Fournisseur', + 'catalog.filter.owner': 'Propriétaire', + 'catalog.filter.tag': 'Étiquette', + 'catalog.filter.clearAll': 'Tout effacer', + 'catalog.card.assetDetailsTitle': 'Détails de la ressource IA', + 'catalog.card.descriptionLabel': 'Description', + 'catalog.card.viewDetails': 'Voir les détails de {{title}}', + 'catalog.card.tagsLabel': 'Étiquettes', + 'catalog.card.providerLabel': 'Fournisseur', + 'catalog.card.usageTitle': 'Utilisation', + 'catalog.card.versionLabel': 'Version', + 'catalog.card.usageDownloadZip': 'Télécharger le ZIP', + 'catalog.card.usageViewSource': 'Voir la source', + 'catalog.card.serverTypeLabel': 'Type de serveur', + 'catalog.card.apiKeyLabel': 'Clé API requise', + 'catalog.card.defaultModelLabel': 'Modèle par défaut', + 'catalog.card.rationaleLabel': 'Justification', + 'catalog.card.disciplinesLabel': 'Disciplines', + 'catalog.card.categoriesLabel': 'Catégories', + 'catalog.card.relatedAgentsLabel': 'Agents associés', + 'catalog.card.ruleCategoryLabel': 'Catégorie de règle', + 'catalog.card.toolsLabel': 'Outils', + 'catalog.card.remotesLabel': "Points d'accès distants", + 'catalog.card.definitionLabel': 'Définition', + 'catalog.card.modelsTitle': 'Modèles', + 'catalog.card.modelTitle': 'Modèle', + 'catalog.card.viewModels': 'Voir tous les modèles', + 'catalog.card.modelsDialogTitle': 'Modèles disponibles', + 'catalog.card.modelSearch': 'Rechercher des modèles', + 'catalog.card.noModelsMatch': + 'Aucun modèle ne correspond à votre recherche.', + 'catalog.card.instructionsTitle': "Instructions de l'agent", + 'catalog.card.handoffDescriptionTitle': 'Description du transfert', + 'catalog.card.handoffTargetsTitle': 'Cibles du transfert', + 'catalog.card.ragEnabledLabel': 'RAG activé', + 'catalog.card.yes': 'Oui', + 'catalog.card.no': 'Non', + 'catalog.table.name': 'Nom', + 'catalog.table.type': 'Type', + 'catalog.table.owner': 'Propriétaire', + 'catalog.table.provider': 'Fournisseur', + 'catalog.table.description': 'Description', + 'catalog.empty.title': 'Aucune ressource IA disponible', + 'catalog.empty.description': + 'Les ressources IA apparaissent ici après avoir été publiées ou synchronisées depuis votre catalogue. Lors du premier chargement, cela peut prendre un moment.', + 'catalog.empty.refresh': 'Actualiser', + 'catalog.empty.learnMore': 'Comment publier', + 'catalog.emptyFiltered.title': + 'Aucune ressource IA ne correspond à vos filtres', + 'catalog.emptyFiltered.description': + "Essayez d'ajuster vos critères de recherche ou de filtre pour trouver ce que vous cherchez.", + 'catalog.emptyFiltered.clearFilters': 'Effacer les filtres', + 'catalog.error.title': 'Échec du chargement des ressources IA', + 'catalog.error.description': + 'Un problème est survenu lors de la connexion au catalogue. Vérifiez votre connexion réseau et réessayez.', + 'catalog.error.retry': 'Réessayer', + 'nav.aiCatalog': 'Catalogue IA', + }, +}); + +export default boostTranslationFr; diff --git a/workspaces/boost/plugins/boost/src/translations/index.ts b/workspaces/boost/plugins/boost/src/translations/index.ts index a4da031a782..ff979f9b7fe 100644 --- a/workspaces/boost/plugins/boost/src/translations/index.ts +++ b/workspaces/boost/plugins/boost/src/translations/index.ts @@ -26,7 +26,13 @@ import { boostTranslationRef } from './ref'; */ export const boostTranslations = createTranslationResource({ ref: boostTranslationRef, - translations: {}, + translations: { + de: () => import('./de'), + es: () => import('./es'), + fr: () => import('./fr'), + it: () => import('./it'), + ja: () => import('./ja'), + }, }); /** @public */ diff --git a/workspaces/boost/plugins/boost/src/translations/it.ts b/workspaces/boost/plugins/boost/src/translations/it.ts new file mode 100644 index 00000000000..d130a69dc44 --- /dev/null +++ b/workspaces/boost/plugins/boost/src/translations/it.ts @@ -0,0 +1,96 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; +import { boostTranslationRef } from './ref'; + +/** + * it translation for plugin.boost. + * @public + */ +const boostTranslationIt = createTranslationMessages({ + ref: boostTranslationRef, + messages: { + 'catalog.page.title': 'Catalogo IA', + 'catalog.toolbar.allPrefix': 'Tutti', + 'catalog.toolbar.search': 'Cerca', + 'catalog.toolbar.viewGrid': 'Vista a schede', + 'catalog.toolbar.viewTable': 'Vista a tabella', + 'catalog.toolbar.filters': 'Filtri', + 'catalog.filter.title': 'Filtri', + 'catalog.filter.all': 'Tutti', + 'catalog.filter.type': 'Tipo', + 'catalog.filter.provider': 'Fornitore', + 'catalog.filter.owner': 'Proprietario', + 'catalog.filter.tag': 'Tag', + 'catalog.filter.clearAll': 'Cancella tutto', + 'catalog.card.assetDetailsTitle': 'Dettagli risorsa IA', + 'catalog.card.descriptionLabel': 'Descrizione', + 'catalog.card.viewDetails': 'Visualizza dettagli di {{title}}', + 'catalog.card.tagsLabel': 'Tag', + 'catalog.card.providerLabel': 'Fornitore', + 'catalog.card.usageTitle': 'Utilizzo', + 'catalog.card.versionLabel': 'Versione', + 'catalog.card.usageDownloadZip': 'Scarica ZIP', + 'catalog.card.usageViewSource': 'Visualizza sorgente', + 'catalog.card.serverTypeLabel': 'Tipo di server', + 'catalog.card.apiKeyLabel': 'Chiave API richiesta', + 'catalog.card.defaultModelLabel': 'Modello predefinito', + 'catalog.card.rationaleLabel': 'Motivazione', + 'catalog.card.disciplinesLabel': 'Discipline', + 'catalog.card.categoriesLabel': 'Categorie', + 'catalog.card.relatedAgentsLabel': 'Agenti correlati', + 'catalog.card.ruleCategoryLabel': 'Categoria di regola', + 'catalog.card.toolsLabel': 'Strumenti', + 'catalog.card.remotesLabel': 'Endpoint remoti', + 'catalog.card.definitionLabel': 'Definizione', + 'catalog.card.modelsTitle': 'Modelli', + 'catalog.card.modelTitle': 'Modello', + 'catalog.card.viewModels': 'Visualizza tutti i modelli', + 'catalog.card.modelsDialogTitle': 'Modelli disponibili', + 'catalog.card.modelSearch': 'Cerca modelli', + 'catalog.card.noModelsMatch': + 'Nessun modello corrisponde alla tua ricerca.', + 'catalog.card.instructionsTitle': "Istruzioni dell'agente", + 'catalog.card.handoffDescriptionTitle': 'Descrizione del trasferimento', + 'catalog.card.handoffTargetsTitle': 'Destinazioni del trasferimento', + 'catalog.card.ragEnabledLabel': 'RAG abilitato', + 'catalog.card.yes': 'Sì', + 'catalog.card.no': 'No', + 'catalog.table.name': 'Nome', + 'catalog.table.type': 'Tipo', + 'catalog.table.owner': 'Proprietario', + 'catalog.table.provider': 'Fornitore', + 'catalog.table.description': 'Descrizione', + 'catalog.empty.title': 'Nessuna risorsa IA disponibile', + 'catalog.empty.description': + 'Le risorse IA appariranno qui dopo essere state pubblicate o sincronizzate dal catalogo. Al primo caricamento, potrebbe richiedere un momento.', + 'catalog.empty.refresh': 'Aggiorna', + 'catalog.empty.learnMore': 'Come pubblicare', + 'catalog.emptyFiltered.title': + 'Nessuna risorsa IA corrisponde ai tuoi filtri', + 'catalog.emptyFiltered.description': + 'Prova a modificare i criteri di ricerca o di filtro per trovare ciò che stai cercando.', + 'catalog.emptyFiltered.clearFilters': 'Cancella filtri', + 'catalog.error.title': 'Impossibile caricare le risorse IA', + 'catalog.error.description': + 'Si è verificato un problema durante la connessione al catalogo. Controlla la connessione di rete e riprova.', + 'catalog.error.retry': 'Riprova', + 'nav.aiCatalog': 'Catalogo IA', + }, +}); + +export default boostTranslationIt; diff --git a/workspaces/boost/plugins/boost/src/translations/ja.ts b/workspaces/boost/plugins/boost/src/translations/ja.ts new file mode 100644 index 00000000000..b9aeb344d90 --- /dev/null +++ b/workspaces/boost/plugins/boost/src/translations/ja.ts @@ -0,0 +1,94 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; +import { boostTranslationRef } from './ref'; + +/** + * ja translation for plugin.boost. + * @public + */ +const boostTranslationJa = createTranslationMessages({ + ref: boostTranslationRef, + messages: { + 'catalog.page.title': 'AIカタログ', + 'catalog.toolbar.allPrefix': 'すべて', + 'catalog.toolbar.search': '検索', + 'catalog.toolbar.viewGrid': 'カードビュー', + 'catalog.toolbar.viewTable': 'テーブルビュー', + 'catalog.toolbar.filters': 'フィルター', + 'catalog.filter.title': 'フィルター', + 'catalog.filter.all': 'すべて', + 'catalog.filter.type': 'タイプ', + 'catalog.filter.provider': 'プロバイダー', + 'catalog.filter.owner': 'オーナー', + 'catalog.filter.tag': 'タグ', + 'catalog.filter.clearAll': 'すべてクリア', + 'catalog.card.assetDetailsTitle': 'AIアセットの詳細', + 'catalog.card.descriptionLabel': '説明', + 'catalog.card.viewDetails': '{{title}} の詳細を表示', + 'catalog.card.tagsLabel': 'タグ', + 'catalog.card.providerLabel': 'プロバイダー', + 'catalog.card.usageTitle': '使用方法', + 'catalog.card.versionLabel': 'バージョン', + 'catalog.card.usageDownloadZip': 'ZIPをダウンロード', + 'catalog.card.usageViewSource': 'ソースを表示', + 'catalog.card.serverTypeLabel': 'サーバータイプ', + 'catalog.card.apiKeyLabel': 'APIキーが必要', + 'catalog.card.defaultModelLabel': 'デフォルトモデル', + 'catalog.card.rationaleLabel': '根拠', + 'catalog.card.disciplinesLabel': '分野', + 'catalog.card.categoriesLabel': 'カテゴリー', + 'catalog.card.relatedAgentsLabel': '関連エージェント', + 'catalog.card.ruleCategoryLabel': 'ルールカテゴリー', + 'catalog.card.toolsLabel': 'ツール', + 'catalog.card.remotesLabel': 'リモートエンドポイント', + 'catalog.card.definitionLabel': '定義', + 'catalog.card.modelsTitle': 'モデル', + 'catalog.card.modelTitle': 'モデル', + 'catalog.card.viewModels': 'すべてのモデルを表示', + 'catalog.card.modelsDialogTitle': '利用可能なモデル', + 'catalog.card.modelSearch': 'モデルを検索', + 'catalog.card.noModelsMatch': '検索に一致するモデルはありません。', + 'catalog.card.instructionsTitle': 'エージェントの指示', + 'catalog.card.handoffDescriptionTitle': 'ハンドオフの説明', + 'catalog.card.handoffTargetsTitle': 'ハンドオフ先', + 'catalog.card.ragEnabledLabel': 'RAG有効', + 'catalog.card.yes': 'はい', + 'catalog.card.no': 'いいえ', + 'catalog.table.name': '名前', + 'catalog.table.type': 'タイプ', + 'catalog.table.owner': 'オーナー', + 'catalog.table.provider': 'プロバイダー', + 'catalog.table.description': '説明', + 'catalog.empty.title': '利用可能なAIアセットはありません', + 'catalog.empty.description': + 'AIアセットは、カタログから公開または同期された後にここに表示されます。初回の読み込みには少し時間がかかる場合があります。', + 'catalog.empty.refresh': '更新', + 'catalog.empty.learnMore': '公開方法を学ぶ', + 'catalog.emptyFiltered.title': 'フィルターに一致するAIアセットはありません', + 'catalog.emptyFiltered.description': + '検索条件やフィルター条件を調整して、お探しのものを見つけてください。', + 'catalog.emptyFiltered.clearFilters': 'フィルターをクリア', + 'catalog.error.title': 'AIアセットの読み込みに失敗しました', + 'catalog.error.description': + 'カタログへの接続中に問題が発生しました。ネットワーク接続を確認して、もう一度お試しください。', + 'catalog.error.retry': '再試行', + 'nav.aiCatalog': 'AIカタログ', + }, +}); + +export default boostTranslationJa; diff --git a/workspaces/boost/plugins/boost/src/translations/ref.test.ts b/workspaces/boost/plugins/boost/src/translations/ref.test.ts new file mode 100644 index 00000000000..a9d2977134e --- /dev/null +++ b/workspaces/boost/plugins/boost/src/translations/ref.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { boostMessages } from './ref'; +import boostTranslationDe from './de'; +import boostTranslationEs from './es'; +import boostTranslationFr from './fr'; +import boostTranslationIt from './it'; +import boostTranslationJa from './ja'; + +function flattenMessages( + obj: Record, + prefix = '', +): Record { + const flattened: Record = {}; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const value = obj[key]; + const newKey = prefix ? `${prefix}.${key}` : key; + if ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) + ) { + Object.assign( + flattened, + flattenMessages(value as Record, newKey), + ); + } else { + flattened[newKey] = String(value); + } + } + } + return flattened; +} + +const refKeys = new Set( + Object.keys(flattenMessages(boostMessages as Record)), +); +const refKeysSorted = Array.from(refKeys).sort(); +const refFlattened = flattenMessages(boostMessages as Record); + +const languageModules = [ + ['de', boostTranslationDe.messages], + ['es', boostTranslationEs.messages], + ['fr', boostTranslationFr.messages], + ['it', boostTranslationIt.messages], + ['ja', boostTranslationJa.messages], +] as const; + +describe('ref (translation keys)', () => { + it('has at least one key', () => { + expect(refKeys.size).toBeGreaterThan(0); + }); + + describe.each(languageModules)('"%s" translations', (_lang, messages) => { + describe('has exactly the same keys as ref (no more, no less)', () => { + const langKeys = Object.keys(messages); + const langKeysSet = new Set(langKeys); + const langKeysSorted = [...langKeys].sort(); + + const missing = refKeysSorted.filter(k => !langKeysSet.has(k)); + const extra = langKeysSorted.filter(k => !refKeys.has(k)); + + it('should have no missing keys', () => { + expect(missing).toEqual([]); + }); + + it('should have no extra keys', () => { + expect(extra).toEqual([]); + }); + + it('should have the same number of keys as ref', () => { + expect(langKeys).toHaveLength(refKeys.size); + }); + }); + + it('preserves interpolation placeholders', () => { + const placeholderRe = /\{\{(\w+)\}\}/g; + for (const [key, value] of Object.entries(messages)) { + const refValue = refFlattened[key]; + if (!refValue) continue; + const refPlaceholders = [...refValue.matchAll(placeholderRe)].map( + m => m[1], + ); + const langPlaceholders = [ + ...(value as string).matchAll(placeholderRe), + ].map(m => m[1]); + expect(langPlaceholders.sort()).toEqual(refPlaceholders.sort()); + } + }); + }); +}); diff --git a/workspaces/boost/specifications/CURRENT.md b/workspaces/boost/specifications/CURRENT.md index 13b7cd9d30e..cf73e19fec3 100644 --- a/workspaces/boost/specifications/CURRENT.md +++ b/workspaces/boost/specifications/CURRENT.md @@ -69,14 +69,18 @@ schema for both. That raises the cost of removal — the fallback code, its tests, the declared schema, and the OGX spec would all have to be updated together. -## Active remaining frontend work (`openspec/changes/`) +## Completed frontend work -| Change | Status | -| ---------------------------------- | ------------------- | -| `ai-catalog-frontend-translations` | 1/11 — locale files | +Frontend translations (de, es, fr, it, ja) are implemented and archived +in `openspec/specs/ai-catalog-translations/`. Playwright locale coverage +is included. Runtime verification of locale switching and English fallback +(archived tasks 10–11) is deferred to a live RHDH environment; Backstage's +`createTranslationRef` provides English fallback by design. -Playwright coverage from PR #4501 is implemented. Its test-infrastructure -change is archived without adding a product-behavior spec. +Category badge labels (`categoryMeta.ts`) are entity-type taxonomy +identifiers and are intentionally not covered by the translation resource. + +There is no remaining active frontend work in `openspec/changes/`. ## RBAC follow-on work @@ -121,4 +125,4 @@ do not add RBAC behavior to this release. Done: inventory; archive implemented frontend, OGX, and E2E work; classify the remaining OpenSpecs; and align workspace documentation with the current code. -Remaining current-release OpenSpec work: frontend translations. +All current-release OpenSpec work is complete. diff --git a/workspaces/boost/specifications/boost-frontend-architecture.md b/workspaces/boost/specifications/boost-frontend-architecture.md index 89433404404..9ce90203fda 100644 --- a/workspaces/boost/specifications/boost-frontend-architecture.md +++ b/workspaces/boost/specifications/boost-frontend-architecture.md @@ -111,9 +111,14 @@ plugins/boost/ entityFiltering.ts entityLinks.ts usageActions.ts - translations/ # English scaffold; locales are a remaining change + translations/ # i18n: en (ref), de, es, fr, it, ja index.ts ref.ts + de.ts + es.ts + fr.ts + it.ts + ja.ts ``` There is no `BoostApiClient`, `useFeatureFlags`, `usePermissions`, or `chat/` / `admin/` source tree in this plugin today. @@ -303,18 +308,18 @@ The AI Catalog is the first domain. Here is how future capabilities map to surfa ## Technology Stack -| Layer | Technology | -| ----------------- | --------------------------------------------------------------------------------------------------------------- | -| Component library | BUI (`@backstage/ui`) for new components, MUI v5 fallback where BUI lacks coverage, `@remixicon/react` icons | -| Chat UI | `@patternfly/chatbot` for conversational interfaces | -| Styling | CSS Modules with `--bui-*` CSS variables | -| Frontend system | NFS Blueprints (`createFrontendPlugin`, `PageBlueprint`, `EntityCardBlueprint`, etc.) | -| State | React hooks + URL params for filters; streaming reducer for chat events | -| API | `catalogApiRef` for catalog entity queries; `fetchApi` for authenticated fetches. No Boost API client yet | -| Testing | Unit: `TestApiProvider` + `renderInTestApp`; Playwright E2E covers primary browse flows in `e2e-tests/` on NFS. | -| i18n | `TranslationBlueprint` + `useTranslationRef`; 5 locales planned (de, es, fr, it, ja) | -| Dynamic plugins | NFS Module Federation via `rhdh-cli plugin export`; no Scalprum (NFS-only plugin) | -| Accessibility | WCAG 2.1 AA, keyboard navigation, screen reader support | +| Layer | Technology | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Component library | BUI (`@backstage/ui`) for new components, MUI v5 fallback where BUI lacks coverage, `@remixicon/react` icons | +| Chat UI | `@patternfly/chatbot` for conversational interfaces | +| Styling | CSS Modules with `--bui-*` CSS variables | +| Frontend system | NFS Blueprints (`createFrontendPlugin`, `PageBlueprint`, `EntityCardBlueprint`, etc.) | +| State | React hooks + URL params for filters; streaming reducer for chat events | +| API | `catalogApiRef` for catalog entity queries; `fetchApi` for authenticated fetches. No Boost API client yet | +| Testing | Unit: `TestApiProvider` + `renderInTestApp`; Playwright E2E covers primary browse flows in `e2e-tests/` on NFS. | +| i18n | `TranslationBlueprint` + `useTranslationRef`; 6 locales (en + de, es, fr, it, ja). Category badge labels (`categoryMeta.ts`) are not translated — they are entity-type taxonomy identifiers. | +| Dynamic plugins | NFS Module Federation via `rhdh-cli plugin export`; no Scalprum (NFS-only plugin) | +| Accessibility | WCAG 2.1 AA, keyboard navigation, screen reader support | ---