From 95a4646aa7ca27d6d4bb29bf280a2f2863929cf3 Mon Sep 17 00:00:00 2001 From: Gerry-Forde <63045020+Gerry-Forde@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:35:02 +0000 Subject: [PATCH] RHIDP-8635-3 - Comprehensive documentation for developers on adding localization support to custom plugins --- ...-configuring-a-floating-action-button.adoc | 4 + .../assembly-configuring-the-quickstarts.adoc | 2 +- .../assembly-customizing-the-appearance.adoc | 2 + assemblies/assembly-localization-in-rhdh.adoc | 26 ++ images/rhdh/customize-language-dropdown.png | Bin 0 -> 51705 bytes ...upport-for-the-floating-action-button.adoc | 60 ++++ ...ing-action-button-as-a-dynamic-plugin.adoc | 40 +++ ...ref-floating-action-button-parameters.adoc | 12 + .../con-language-persistence.adoc | 27 ++ ...adding-localization-to-custom-plugins.adoc | 276 ++++++++++++++++++ .../proc-customize-rhdh-language.adoc | 26 ++ ...-enabling-localization-in-quickstarts.adoc | 89 ++++++ .../proc-enabling-localization-in-rhdh.adoc | 28 ++ ...nabling-localization-in-sidebar-items.adoc | 49 ++++ .../proc-overriding-translations.adoc | 120 ++++++++ .../proc-select-rhdh-language.adoc | 21 ++ .../ref-best-practices-for-localization.adoc | 34 +++ titles/customizing/master.adoc | 2 + 18 files changed, 817 insertions(+), 1 deletion(-) create mode 100644 assemblies/assembly-localization-in-rhdh.adoc create mode 100644 images/rhdh/customize-language-dropdown.png create mode 100644 modules/configuring-a-floating-action-button/con-localization-support-for-the-floating-action-button.adoc create mode 100644 modules/customizing-the-appearance/con-language-persistence.adoc create mode 100644 modules/customizing-the-appearance/proc-adding-localization-to-custom-plugins.adoc create mode 100644 modules/customizing-the-appearance/proc-customize-rhdh-language.adoc create mode 100644 modules/customizing-the-appearance/proc-enabling-localization-in-quickstarts.adoc create mode 100644 modules/customizing-the-appearance/proc-enabling-localization-in-rhdh.adoc create mode 100644 modules/customizing-the-appearance/proc-enabling-localization-in-sidebar-items.adoc create mode 100644 modules/customizing-the-appearance/proc-overriding-translations.adoc create mode 100644 modules/customizing-the-appearance/proc-select-rhdh-language.adoc create mode 100644 modules/customizing-the-appearance/ref-best-practices-for-localization.adoc diff --git a/assemblies/assembly-configuring-a-floating-action-button.adoc b/assemblies/assembly-configuring-a-floating-action-button.adoc index 319cb61067..d2d36469d2 100644 --- a/assemblies/assembly-configuring-a-floating-action-button.adoc +++ b/assemblies/assembly-configuring-a-floating-action-button.adoc @@ -8,4 +8,8 @@ You can use the floating action button plugin to configure any action as a float include::modules/configuring-a-floating-action-button/proc-configuring-floating-action-button-as-a-dynamic-plugin.adoc[leveloffset=+1] +// Localization +include::modules/configuring-a-floating-action-button/con-localization-support-for-the-floating-action-button.adoc[leveloffset=+1] +// END Localization + include::modules/configuring-a-floating-action-button/ref-floating-action-button-parameters.adoc[leveloffset=+1] diff --git a/assemblies/assembly-configuring-the-quickstarts.adoc b/assemblies/assembly-configuring-the-quickstarts.adoc index 34d76f4d57..59f87c0a84 100644 --- a/assemblies/assembly-configuring-the-quickstarts.adoc +++ b/assemblies/assembly-configuring-the-quickstarts.adoc @@ -12,4 +12,4 @@ include::modules/configuring-the-quickstarts/proc-disabling-rhdh-quickstart.adoc include::modules/configuring-the-quickstarts/proc-starting-and-completing-modules-in-quickstarts.adoc[leveloffset=+1] -include::modules/configuring-the-quickstarts/con-using-rbac-with-quickstarts.adoc[leveloffset=+1] +include::modules/customizing-the-appearance/proc-enabling-localization-in-quickstarts.adoc[leveloffset=+1] diff --git a/assemblies/assembly-customizing-the-appearance.adoc b/assemblies/assembly-customizing-the-appearance.adoc index 2c69959e63..04358576b8 100644 --- a/assemblies/assembly-customizing-the-appearance.adoc +++ b/assemblies/assembly-customizing-the-appearance.adoc @@ -32,6 +32,8 @@ include::modules/customizing-the-appearance/con-about-rhdh-sidebar-menuitems.ado include::modules/customizing-the-appearance/proc-customize-rhdh-sidebar-menuitems.adoc[leveloffset=+2] +include::modules/customizing-the-appearance/proc-enabling-localization-in-sidebar-items.adoc[leveloffset=+2] + include::modules/customizing-the-appearance/proc-configuring-dynamic-plugin-menuitem.adoc[leveloffset=+2] include::modules/customizing-the-appearance/proc-modifying-or-adding-rhdh-custom-menuitem.adoc[leveloffset=+2] diff --git a/assemblies/assembly-localization-in-rhdh.adoc b/assemblies/assembly-localization-in-rhdh.adoc new file mode 100644 index 0000000000..8785703aef --- /dev/null +++ b/assemblies/assembly-localization-in-rhdh.adoc @@ -0,0 +1,26 @@ +:_mod-docs-content-type: ASSEMBLY +:context: assembly-localization-in-rhdh + +[id="assembly-localization-in-rhdh_{context}"] += Localization in {product} + +include::modules/customizing-the-appearance/proc-enabling-localization-in-rhdh.adoc[leveloffset=+1] + +include::modules/customizing-the-appearance/proc-select-rhdh-language.adoc[leveloffset=+1] + +include::modules/customizing-the-appearance/con-language-persistence.adoc[leveloffset=+2] + +// include::modules/customizing-the-appearance/proc-enabling-localization-in-quickstarts.adoc[leveloffset=+1] + +// include::modules/customizing-the-appearance/proc-enabling-localization-in-sidebar-items.adoc[leveloffset=+1] + +//include::modules/customizing-the-appearance/proc-enabling-localization-in-floating-action-button.adoc[leveloffset=+1] + +== Localization support for plugins + +include::modules/customizing-the-appearance/proc-overriding-translations.adoc[leveloffset=+2] + +include::modules/customizing-the-appearance/ref-best-practices-for-localization.adoc[leveloffset=+2] + +include::modules/customizing-the-appearance/proc-adding-localization-to-custom-plugins.adoc[leveloffset=+2] + diff --git a/images/rhdh/customize-language-dropdown.png b/images/rhdh/customize-language-dropdown.png new file mode 100644 index 0000000000000000000000000000000000000000..ab668d60f32c6da6801c2b1748a1bff0e689e11d GIT binary patch literal 51705 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 I3;x&t0An+OAOHXW literal 0 HcmV?d00001 diff --git a/modules/configuring-a-floating-action-button/con-localization-support-for-the-floating-action-button.adoc b/modules/configuring-a-floating-action-button/con-localization-support-for-the-floating-action-button.adoc new file mode 100644 index 0000000000..6794473a93 --- /dev/null +++ b/modules/configuring-a-floating-action-button/con-localization-support-for-the-floating-action-button.adoc @@ -0,0 +1,60 @@ +[id="proc-enabling-localization-in-floating-action-button_{context}"] += Enabling floating action button localization in {product-very-short} + +You can enable translation key support for floating action buttons, so that users can onboard in their preferred language. In {product-short}, all existing and newly created floating action buttons support localization using dedicated translation keys. + +The Global Floating Action Button plugin supports internationalization (i18n) through translation keys. You can use `labelKey` and `toolTipKey` properties to provide translation keys instead of static text. + +The plugin provides the following built-in translation keys organized under the `fab` namespace: + +* `fab.create.label` - "Create" +* `fab.create.tooltip` - "Create entity" +* `fab.docs.label` - "Docs" +* `fab.docs.tooltip` - "Documentation" +* `fab.apis.label` - "APIs" +* `fab.apis.tooltip` - "API Documentation" +* `fab.github.label` - "GitHub" +* `fab.github.tooltip` - "GitHub Repository" +* `fab.bulkImport.label` - "Bulk Import" +* `fab.bulkImport.tooltip` - "Register multiple repositories in bulk" +* `fab.quay.label` - "Quay" +* `fab.quay.tooltip` - "Quay Container Registry" + +The plugin includes translations for the following supported languages: + +* English (default) +// * German (de) +* French (fr) +// * Spanish (es) + +// [NOTE] +// ==== +// To add localization support for a new floating action button item, you can add any arbitrary key name for the `labelKey` and `toolTipKey` properties and provide corresponding translations for those keys. + +// If you add a new floating action button item, you can add localization support by adding an arbitary label key and tool tip key + +// ==== + +To ensure backward compatibility while providing translation support when available, the following order is used to resolve string translations: + +. If the `labelKey` is provided, the plugin will attempt to resolve the translation key +. If the translation key is found, it will be used as the label +. If the translation key is not found, the plugin will fall back to the label property + +[NOTE] +==== +The same logic applies to `toolTipKey` and `toolTip`. +==== + +== Internal translation implementation +The plugin uses a centralized translation system where: + +* The `useTranslation()` hook is called in components that render floating action buttons to ensure proper translation context initialization +* The translation function (`t`) is passed down to child components that need to resolve translation keys +* This internal architecture prevents infinite re-render loops and ensures stable component rendering +* All components that use `CustomFab` must provide the translation function as a prop + +[NOTE] +==== +When extending or modifying the plugin components, ensure that the `useTranslation()` hook is called in parent components and the `t` prop is passed to `CustomFab` instances to maintain proper translation functionality and prevent rendering issues. +==== \ No newline at end of file diff --git a/modules/configuring-a-floating-action-button/proc-configuring-floating-action-button-as-a-dynamic-plugin.adoc b/modules/configuring-a-floating-action-button/proc-configuring-floating-action-button-as-a-dynamic-plugin.adoc index 4193f14924..a523dc2c5e 100644 --- a/modules/configuring-a-floating-action-button/proc-configuring-floating-action-button-as-a-dynamic-plugin.adoc +++ b/modules/configuring-a-floating-action-button/proc-configuring-floating-action-button-as-a-dynamic-plugin.adoc @@ -203,3 +203,43 @@ To configure a floating action button as a dynamic plugin, complete any of the f text: Bulk import ---- `frontend:mountPoints:importName`:: Enter the import name with an associated component to the mount point. + += Translation support +The Global Floating Action Button plugin supports internationalization (i18n) through translation keys. You can use `labelKey` and `toolTipKey` properties to provide translation keys instead of static text. + +Example for using translation keys in dynamic configuration: +[source,yaml] +---- +- package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-global-floating-action-button + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-global-floating-action-button: + translationResources: + - importName: globalFloatingActionButtonTranslations + ref: globalFloatingActionButtonTranslationRef + mountPoints: + - mountPoint: application/listener + importName: DynamicGlobalFloatingActionButton + - mountPoint: global.floatingactionbutton/config + importName: NullComponent + config: + icon: github + label: 'GitHub' # Fallback text + labelKey: 'fab.github.label' # Translation key + toolTip: 'GitHub Repository' # Fallback text + toolTipKey: 'fab.github.tooltip' # Translation key + to: https://github.com/redhat-developer/rhdh-plugins + - mountPoint: global.floatingactionbutton/config + importName: NullComponent + config: + color: 'success' + icon: search + label: 'Create' # Fallback text + labelKey: 'fab.create.label' # Translation key + toolTip: 'Create entity' # Fallback text + toolTipKey: 'fab.create.tooltip' # Translation key + to: '/create' + showLabel: true +---- \ No newline at end of file diff --git a/modules/configuring-a-floating-action-button/ref-floating-action-button-parameters.adoc b/modules/configuring-a-floating-action-button/ref-floating-action-button-parameters.adoc index 1f2c3d54e4..d6be21b37c 100644 --- a/modules/configuring-a-floating-action-button/ref-floating-action-button-parameters.adoc +++ b/modules/configuring-a-floating-action-button/ref-floating-action-button-parameters.adoc @@ -21,6 +21,12 @@ Use the parameters as shown in the following table to configure your floating ac | Not applicable | Yes +| `labelKey` +| Translation key for the label. If provided, will be used instead of label when translations are available. +| `String` +| Not applicable +| No + | `icon` | Icon of the floating action button. Recommended to use filled icons from the link:https://fonts.google.com/icons[Material Design library]. You can also use an svg icon. For example: `` | `String`, `React.ReactElement`, `SVG image icon`, `HTML image icon` @@ -63,6 +69,12 @@ Use the parameters as shown in the following table to configure your floating ac | Not applicable | No +| `toolTipKey` +| Translation key for the tooltip. If provided, will be used instead of toolTip when translations are available. +| `String` +| Not applicable +| No + | `priority` | Order of the floating action buttons displayed in the submenu. A larger value means higher priority. | `number` diff --git a/modules/customizing-the-appearance/con-language-persistence.adoc b/modules/customizing-the-appearance/con-language-persistence.adoc new file mode 100644 index 0000000000..5e6097f3d3 --- /dev/null +++ b/modules/customizing-the-appearance/con-language-persistence.adoc @@ -0,0 +1,27 @@ +:_mod-docs-content-type: CONCEPT + +[id="con-language-persistence_{context}"] += Language persistence + +When you change the language in the UI, your preference is saved to storage. On next login or refresh, your chosen language setting is restored. Guest users cannot persist language preferences. + +Default language selection uses the following priority order: + +. *Browser language priority*: The system first checks the user's browser language preferences to provide a personalized experience. + +. *Configuration priority*: If no browser language matches the supported locales, the `defaultLocale` from the `i18n` configuration is used as a fallback. + +. *Fallback priority*: If neither browser preferences nor configuration provide a match, defaults to `en`. + +{product} automatically saves and restores user language settings across browser sessions. This feature is enabled by default and uses database storage. To opt-out and use browser storage instead, add the following to your `{my-app-config-file}` configuration file: +[source,yaml,subs="+quotes"] +---- +userSettings: + persistence: browser +---- + +where: + +userSettings:persistence:: +Enter `browser` to opt-out and use browser local storage. Optionally, set this value to `database` to persist across browsers and devices. This is the default setting and does not require this configuration to be set. + diff --git a/modules/customizing-the-appearance/proc-adding-localization-to-custom-plugins.adoc b/modules/customizing-the-appearance/proc-adding-localization-to-custom-plugins.adoc new file mode 100644 index 0000000000..73be45d47d --- /dev/null +++ b/modules/customizing-the-appearance/proc-adding-localization-to-custom-plugins.adoc @@ -0,0 +1,276 @@ +:_mod-docs-content-type: PROCEDURE + +[id="proc-adding-localization-to-custom-plugins_{context}"] += Implementing localization support for your custom plugins +You can implement localization support in your custom {product-very-short} plugins so that your plugins are accessible to a diverse, international user base and follow recommended best practices. + +.Procedure +. Create the following translation files in your plugin's `src/translations/` directory: ++ +.`src/translations/ref.ts` English reference +[source,json] +---- +import { createTranslationRef } from "@backstage/core-plugin-api/alpha"; + +export const myPluginMessages = { + page: { + title: "My Plugin", + subtitle: "Plugin description", + }, + common: { + exportCSV: "Export CSV", + noResults: "No results found", + }, + table: { + headers: { + name: "Name", + count: "Count", + }, + }, +}; + +export const myPluginTranslationRef = createTranslationRef({ + id: "plugin.my-plugin", + messages: myPluginMessages, +}); +---- ++ +.`src/translations/de.ts` German translation +[source,json] +---- +import { createTranslationMessages } from "@backstage/core-plugin-api/alpha"; +import { myPluginTranslationRef } from "./ref"; + +const myPluginTranslationDe = createTranslationMessages({ + ref: myPluginTranslationRef, + messages: { + "page.title": "Mein Plugin", + "page.subtitle": "Plugin-Beschreibung", + "common.exportCSV": "CSV exportieren", + "common.noResults": "Keine Ergebnisse gefunden", + "table.headers.name": "Name", + "table.headers.count": "Anzahl", + }, +}); + +export default myPluginTranslationDe; +---- ++ +.`src/translations/fr.ts` French translation +[source,json] +---- +import { createTranslationMessages } from "@backstage/core-plugin-api/alpha"; +import { myPluginTranslationRef } from "./ref"; + +const myPluginTranslationFr = createTranslationMessages({ + ref: myPluginTranslationRef, + messages: { + "page.title": "Mon Plugin", + "page.subtitle": "Description du plugin", + "common.exportCSV": "Exporter CSV", + "common.noResults": "Aucun résultat trouvé", + "table.headers.name": "Nom", + "table.headers.count": "Nombre", + }, +}); + +export default myPluginTranslationFr; +---- ++ +.`src/translations/index.ts` Translation resource +[source,json] +---- +import { createTranslationResource } from "@backstage/core-plugin-api/alpha"; +import { myPluginTranslationRef } from "./ref"; + +export const myPluginTranslations = createTranslationResource({ + ref: myPluginTranslationRef, + translations: { + de: () => import("./de"), + fr: () => import("./fr"), + }, +}); + +export { myPluginTranslationRef }; +---- + +. Create translation hooks file, as follows: ++ +.`src/hooks/useTranslation.ts` Translation hooks +[source,json] +---- +import { useTranslationRef } from "@backstage/core-plugin-api/alpha"; +import { myPluginTranslationRef } from "../translations"; + +export const useTranslation = () => useTranslationRef(myPluginTranslationRef); +---- + +. Update your plugin components to replace hard-coded strings with translation calls as shown in the following example: ++ +.Before (hardcoded): +[source,json] +---- +const MyComponent = () => { + return ( +
+

My Plugin

+ +
+ ); +}; +---- ++ +.After (translated): +[source,json] +---- +import { useTranslation } from '../hooks/useTranslation'; + +const MyComponent = () => { + const { t } = useTranslation(); + + return ( +
+

{t('page.title')}

+ +
+ ); +}; +---- + +. (Optional) If your content contains variables, use interpolation: ++ +[source,json] +---- +// In your translation files +'table.pagination.topN': 'Top {{count}} items' + +// In your component +const { t } = useTranslation(); +const message = t('table.pagination.topN', { count: '10' }); +---- + +. (Optional) If your content contains dynamic translation keys (for example, from your plugin configuration): ++ +[source,json] +---- +// Configuration object with translation keys +const CARD_CONFIGS = [ + { id: 'overview', titleKey: 'cards.overview.title' }, + { id: 'details', titleKey: 'cards.details.title' }, + { id: 'settings', titleKey: 'cards.settings.title' }, +]; + +// In your component +const { t } = useTranslation(); + +const CardComponent = ({ config }) => { + return ( +
+

{t(config.titleKey as any)}

+ {/* Use 'as any' for dynamic keys */} +
+ ); +}; +---- + +. Export the translation resources ++ +[source,json] +.`src/alpha.ts` file fragment +---- +// Export your plugin +export { myPlugin } from "./plugin"; + +// Export translation resources for RHDH +export { myPluginTranslations, myPluginTranslationRef } from "./translations"; +---- + +. Update your `dynamic-plugins.default.yaml` file, as follows: ++ +[source,json] +.`dynamic-plugins.default.yaml` file fragment +---- +backstage-community.plugin-my-plugin: + translationResources: + - importName: myPluginTranslations + ref: myPluginTranslationRef + module: Alpha +---- + +. Update your `package.json` file as follows: ++ +[source,json] +.`package.json` file fragment +---- +"exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "main": "src/index.ts", + "types": "src/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + } +---- + +.Verification +To verify your translations, create a test mock file. For example: + +.`src/test-utils/mockTranslations.ts` Test mock file +[source,json] +---- +import { myPluginMessages } from "../translations/ref"; + +function flattenMessages(obj: any, prefix = ""): Record { + const flattened: Record = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const value = obj[key]; + const newKey = prefix ? `${prefix}.${key}` : key; + if (typeof value === "object" && value !== null) { + Object.assign(flattened, flattenMessages(value, newKey)); + } else { + flattened[newKey] = value; + } + } + } + return flattened; +} + +const flattenedMessages = flattenMessages(myPluginMessages); + +export const mockT = (key: string, params?: any) => { + let message = flattenedMessages[key] || key; + if (params) { + for (const [paramKey, paramValue] of Object.entries(params)) { + message = message.replace( + new RegExp(`{{${paramKey}}}`, "g"), + String(paramValue), + ); + } + } + return message; +}; + +export const mockUseTranslation = () => ({ t: mockT }); +---- + +.Update your tests +[source,json] +---- +import { mockUseTranslation } from "../test-utils/mockTranslations"; + +jest.mock("../hooks/useTranslation", () => ({ + useTranslation: mockUseTranslation, +})); + +// Your test code... +---- \ No newline at end of file diff --git a/modules/customizing-the-appearance/proc-customize-rhdh-language.adoc b/modules/customizing-the-appearance/proc-customize-rhdh-language.adoc new file mode 100644 index 0000000000..a64e242f67 --- /dev/null +++ b/modules/customizing-the-appearance/proc-customize-rhdh-language.adoc @@ -0,0 +1,26 @@ +:_mod-docs-content-type: PROCEDURE + +[id="proc-customize-rhdh-language_{context}"] += Customizing the language for your {product-short} instance + +The language settings of {product-very-short} use English by default. You can choose to use one of the following languages instead. + +.Supported languages +* English +* French + +[NOTE] +==== +English and French are the supported languages in {product-very-short} 1.8. You can add other languages in the the `i18n` section of your `{my-app-config-file}` configuration file. +==== + +.Prerequisites + +* You are logged in to the {product-short} web console. + +.Procedure + +. From the {product-short} web console, click *Settings*. +. From the *Appearance* panel, click the language dropdown to select your language of choice. ++ +image::rhdh/customize-language-dropdown.png[] \ No newline at end of file diff --git a/modules/customizing-the-appearance/proc-enabling-localization-in-quickstarts.adoc b/modules/customizing-the-appearance/proc-enabling-localization-in-quickstarts.adoc new file mode 100644 index 0000000000..52033fbb39 --- /dev/null +++ b/modules/customizing-the-appearance/proc-enabling-localization-in-quickstarts.adoc @@ -0,0 +1,89 @@ +:_mod-docs-content-type: CONCEPT + +[id="proc-enabling-localization-in-quickstarts_{context}"] += Enabling Quickstart localization in {product-very-short} + +You can enable translation key support for Quickstart titles, descriptions, and CTAs, so that users can onboard in their preferred language. In {product-short}, all existing and newly created Quickstart steps support localization using dedicated translation keys (`titleKey`, `descriptionKey`, `cta.textKey`). + +[NOTE] +==== +If a translation key is present but the corresponding localized string is missing, the system defaults to the original text defined in the Quickstart configuration (`title`, `description`, `text`). If no translation key is defined at all, the original text is displayed. +==== + +.Prerequisites +* You have enabled localization in your {product-very-short} application. + +.Procedure + +. For *all* Quickstart steps (both existing and new) in your configuration file, you must define both the original text and the new localization keys. For example, in the `quickstart` section of your custom `{my-app-config-file}` file, add the `titleKey`, `descriptionKey`, and `textKey` values, as follows: ++ +.`{my-app-config-file}` fragment +[source,yaml,subs="+quotes"] +---- +app: + quickstart: + # Existing Quickstart steps should also be updated with keys + - title: 'Setup Authentication' + titleKey: steps.setupAuth.title + description: 'Learn the basics of navigating the Developer Hub interface' + descriptionKey: steps.setupAuth.description + icon: 'home' + cta: + text: 'Get Started' + textKey: steps.setupAuth.ctaTitle + link: '/catalog' +# ... +---- ++ +where: + +`title`:: (Mandatory) Fallback for the title. +`titleKey`:: Key for the translated title. +`description`:: (Mandatory) Fallback for the description. +`descriptionKey`:: Key for the translated description. +`text`:: (Mandatory) Fallback for the CTA text. +`textKey`:: Key for the translated CTA text. + +. In your `dynamic-plugins.yaml` file, add the `translationResources` section to your `red-hat-developer-hub-backstage-plugin-quickstart` configuration, as follows: ++ +.`{my-app-config-file}` fragment +[source,yaml,subs="+quotes"] +---- +plugins: + - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-quickstart + disabled: false + pluginConfig: + dynamicPlugins: + frontend: + red-hat-developer-hub.backstage-plugin-quickstart: + # translationResources definition is required for translations to work + translationResources: + - importName: quickstartTranslations + ref: quickstartTranslationRef + # ... other configurations like mountPoints ... +---- ++ +where: + +importName:: +Enter the name used to reference the import. +ref:: +Reference to the resource definition. +. In your translation file, map the keys from the first step to the localized strings for each supported language. ++ +.`allTranslations.json` fragment +[source,yaml,subs="+quotes"] +---- +"plugin.quickstart": { + "en": { + "steps.setupAuth.title": "Manage plugins EN", + "steps.setupAuth.description": "EN Browse and install extensions to add features, connect with external tools, and customize your experience.", + "steps.setupAuth.ctaTitle": "Start" + }, + "fr": { + "steps.setupAuth.title": "Gérer les plugins FR", + "steps.setupAuth.description": "FR Parcourez et installez des extensions pour ajouter des fonctionnalités, vous connecter à des outils externes et personnaliser votre expérience.", + "steps.setupAuth.ctaTitle": "Commencer" + } +} +---- \ No newline at end of file diff --git a/modules/customizing-the-appearance/proc-enabling-localization-in-rhdh.adoc b/modules/customizing-the-appearance/proc-enabling-localization-in-rhdh.adoc new file mode 100644 index 0000000000..50c1c05076 --- /dev/null +++ b/modules/customizing-the-appearance/proc-enabling-localization-in-rhdh.adoc @@ -0,0 +1,28 @@ +:_mod-docs-content-type: PROCEDURE + +[id="proc-enabling-localization-in-rhdh_{context}"] += Enabling the localization framework in {product-short} +Enabling localization enhances accessibility, improves the user experience for a global audience, and assists organizations in meeting language requirements in specific regions. + +The language settings of {product} ({product-very-short}) use English by default. In {product-very-short} {product-version}, you can choose to use one of the following supported languages: + +* English (en) +* French (fr) + +.Prerequisites + +.Procedure +. To enable the localization framework in your {product-very-short} application, add the `i18n` section to your custom {product-short} `{my-app-config-file}` configuration file: ++ +[id=i18n] +.`{my-app-config-file}` fragment with localization `i18n` fields +[source,yaml,subs="+quotes"] +---- +... +i18n: + locales: # List of supported locales. Must include `en`, otherwise the translation framework will fail to load. + - en + - fr + defaultLocale: en # Optional. Defaults to `en` if not specified. +... +---- \ No newline at end of file diff --git a/modules/customizing-the-appearance/proc-enabling-localization-in-sidebar-items.adoc b/modules/customizing-the-appearance/proc-enabling-localization-in-sidebar-items.adoc new file mode 100644 index 0000000000..175bfcd6df --- /dev/null +++ b/modules/customizing-the-appearance/proc-enabling-localization-in-sidebar-items.adoc @@ -0,0 +1,49 @@ +:_mod-docs-content-type: CONCEPT + +[id="proc-enabling-localization-in-sidebar-menu-items_{context}"] += Enabling sidebar menu items localization in {product-very-short} + +You can add translation key support for sidebar menu items, so that users can onboard in their preferred language. In {product-short}, all existing and newly created sidebar menu items support localization using the `titleKey` translation key. + +[NOTE] +==== +If a translation key is present but the corresponding localized string is missing, the system defaults to the original text defined in the sidebar menu items configuration (`title`). If no translation key is defined at all, the original text is displayed. +==== + +.Prerequisites +* You have enabled localization in your {product-very-short} application. + +.Procedure + +. For sidebar menu items in your configuration file, you must define both the original text and the new localization keys. For example, in the `dynamicPlugins.frontend.default.main-menu-items.menuItems.default.favorites` section of your `{my-app-config-file}` file, add the `titleKey`, as follows: ++ +.Example `{my-app-config-file}` fragment +[source,yaml,subs="+quotes"] +---- +dynamicPlugins: + frontend: + default.main-menu-items: + menuItems: + default.favorites: + title: Favorites + titleKey: menuItem.favorites + icon: favorite + priority: 100 + enabled: true +---- +. In your translation file, map the `titleKey` from the first step to the localized strings for each supported language. ++ +.Example `allTranslations.json` fragment +[source,yaml,subs="+quotes"] +---- +{ + "rhdh": { + "en": { + "menuItem.favorites": "Favorites" + }, + "fr": { + "menuItem.favorites": "Favoris" + } + } +} +---- \ No newline at end of file diff --git a/modules/customizing-the-appearance/proc-overriding-translations.adoc b/modules/customizing-the-appearance/proc-overriding-translations.adoc new file mode 100644 index 0000000000..6de959d813 --- /dev/null +++ b/modules/customizing-the-appearance/proc-overriding-translations.adoc @@ -0,0 +1,120 @@ +:_mod-docs-content-type: CONCEPT + +[id="prov-overriding-translations_{context}"] += Overriding translations +In {product-very-short} 1.8, you can override plugin translation strings without modifying the plugin source code. + +.Prerequisites +* You have enabled localization in your {product-very-short} application. +* For an Operator-installed {product-very-short} instance, you have installed the {openshift-cli}. For more information about installing `oc`, see {ocp-docs-link}/html/cli_tools/openshift-cli-oc#installing-openshift-cli[Installing the OpenShift CLI]. + +.Procedure +// This feature is not being included in 1.8 +// . In the top user menu, go to *Settings* > *General*. +// . Click on the download link in the *Translations* panel to download the default English translation strings. +. Create a JSON file containing the translation strings that you want to override, as shown in the following example: ++ +[id=i18n-enable] +.`allTranslations.json` fragment with translation string overrides +[source,json] +---- +{ + "plugin.global-floating-action-button": { + "en": { + "fab.quay.label": "QUAY EN JSON", + "fab.rbac.label": "RBAC EN JSON", + "fab.rbac.tooltip": "RBAC EN tooltip JSON" + }, + "fr": { + "fab.quay.label": "QUAY French JSON", + "fab.quay.tooltip": "QUAY french tooltip JSON", + "fab.rbac.label": "RBAC French JSON", + "fab.rbac.tooltip": "RBAC french tooltip JSON" + } + }, + "plugin.global-header": { + "en": { + "applicationLauncher.developerHub": "Developer Hub EN JSON" + }, + "fr": { + "applicationLauncher.developerHub": "Developer Hub French JSON" + } + } +} +---- +. Log in to your cluster and create a config map for your translations override strings: ++ +[source,bash] +---- +oc create configmap all-translations \ + --from-file=//allTranslations.json +---- + +. Update your deployment configuration based on your installation method: + +.. For an Operator-installed {product-very-short} instance, update your `{product-custom-resource-type}` custom resource (CR). For more information about configuring a CR, see link:https://docs.redhat.com/en/documentation/red_hat_developer_hub/{product-version}/html/configuring_red_hat_developer_hub/provisioning-and-using-your-custom-configuration#using-the-operator-to-run-rhdh-with-your-custom-configuration[Using the Red Hat Developer Hub Operator to run Developer Hub with your custom configuration]. +... In the `spec.application.extraFiles` section, add the translations custom app configuration as shown in the following example: ++ +.{product-custom-resource-type} custom resource fragment +[source,yaml,subs="+quotes"] +---- +apiVersion: rhdh.redhat.com/v1alpha3 +kind: Backstage +spec: + application: + extraFiles: + mountPath: /opt/app-root/src/translations + configMaps: + - name: all-translations +---- + +.. For a Helm-installed {product-very-short} instance, update your {product-short} `{backstage}` Helm chart to mount in the {product-short} filesystem your files from the `all-translations` config map: + +... In the {product-short} Helm chart, go to *Root Schema* → *Backstage chart schema* → *Backstage parameters* → *Backstage container additional volume mounts*. + +... Select *Add {backstage} container additional volume mounts* and add the following values: + +mountPath:: +`/opt/app-root/src/translations` +name:: +`all-translations` + +... Add the translations to the *{backstage} container additional volumes* in the {product-short} Helm chart: + +name:: +`all-translations` +configMap:: +defaultMode::: + `420` +name::: +`all-translations` + +. Update the `i18n` section to your custom {product-short} `{my-app-config-file}` configuration file to include the following translation override file: ++ +[id=i18n-override] +.`{my-app-config-file}` fragment with localization `i18n` fields +[source,yaml,subs="+quotes"] +---- +i18n: + locales: # List of supported locales. Must include `en`, otherwise the translation framework will fail to load. + - en + - fr + defaultLocale: en # Optional. Defaults to `en` if not specified. + overrides: # List of JSON translation files applied in order (last file wins). Each file may override/add translations for one or more plugins/locales + - /opt/app-root/src/translations/all-translations.json +---- + +.Additional resources +// * link:{customizing-book-link}#configuring-templates[Enabling floating button localization in {product-short}] +// * link:{customizing-book-link}#configuring-templates[Enabling Quickstart localization in {product-short}] +// * link:{customizing-book-link}#configuring-templates[Enabling sidebar menu items localization in {product-short}] + +{context} + +* xref:proc-enabling-localization-in-floating-action-button_configuring-a-floating-action-button[Enabling floating button localization in {product-short}] +* xref:proc-enabling-localization-in-quickstarts_customizing-the-quickstarts[Enabling Quickstart localization in {product-short}] +* xref:proc-enabling-localization-in-sidebar-menu-items_customizing-appearance[Enabling sidebar menu items localization in {product-short}] + +// * xref:proc-enabling-localization-in-floating-action-button_{context}[fggdsg] +// * xref:proc-enabling-localization-in-quickstarts_{context}[fggdsg] +// * xref:proc-enabling-localization-in-sidebar-menu-items_{context}[fggdsg] \ No newline at end of file diff --git a/modules/customizing-the-appearance/proc-select-rhdh-language.adoc b/modules/customizing-the-appearance/proc-select-rhdh-language.adoc new file mode 100644 index 0000000000..3bb91ca3fc --- /dev/null +++ b/modules/customizing-the-appearance/proc-select-rhdh-language.adoc @@ -0,0 +1,21 @@ +:_mod-docs-content-type: PROCEDURE + +[id="proc-selecting-rhdh-language_{context}"] += Selecting the language for your {product-short} instance + +You can choose to use one of the following supported languages: + +* English (default) +* French + +.Prerequisites + +* You are logged in to the {product-short} web console. +* You have xref:proc-enabling-localization-in-rhdh_{context}[enabled the localization framework] in your {product-very-short} instance. + +.Procedure + +. From the {product-short} web console, click the down arrow next to your profile name, then click *Settings*. +. From the *Appearance* panel, click the language dropdown to select your language of choice. ++ +image::rhdh/customize-language-dropdown.png[] \ No newline at end of file diff --git a/modules/customizing-the-appearance/ref-best-practices-for-localization.adoc b/modules/customizing-the-appearance/ref-best-practices-for-localization.adoc new file mode 100644 index 0000000000..a8e1b36646 --- /dev/null +++ b/modules/customizing-the-appearance/ref-best-practices-for-localization.adoc @@ -0,0 +1,34 @@ +:_mod-docs-content-type: REFERENCE + +[id="ref-best-practices-for-localization_{context}"] += Best practices for implementing localization support for custom plugins in {product-very-short} +When you add localization support to your {product-very-short} plugins, the following best practices help ensure that you establish a robust, type-safe, and future-proof localization workflow, separating the immutable source text from the organized key structure, and ensuring reliable deployment across all targeted languages: + +Do not modify original English strings:: This preserves the source of truth for all translators, preventing unexpected changes that would invalidate existing translations and ensuring consistency across all versions. + +Use flat dot notation in translation files:: Flat dot notation, for example `page.title`, follows the standard `i18next` library convention, which optimizes runtime lookups and keeps the actual translation values concise and easy to manage for translation services. + +Use nested objects in the reference file for TypeScript support:: This allows the TypeScript compiler to enforce structural type checking on your translation keys, catching errors during development rather than at runtime. + +Test with mocks to ensure translations work correctly:: This isolates the translation logic, guaranteeing the correct keys are passed and rendered without relying on a full environment setup or external translation files during unit testing. + +Add all languages to your application configuration:: This ensures that the {product-very-short} application initializes and loads all necessary language resources at startup, making the locales immediately available for users to select in the UI. + +.Common patterns + +[cols="20%,35%,45%", frame="all", options="header"] +|=== +| Use case | Pattern | Example + +| Simple text +| `t('key')` +| `t('page.title')` + +| With variables +| `t('key', {param})` +| `t('table.topN', {count: '5'})` + +| Dynamic keys +| `t(config.titleKey as any)` +| `t('cards.overview.title' as any)` +|=== \ No newline at end of file diff --git a/titles/customizing/master.adoc b/titles/customizing/master.adoc index db68b5602e..2ebd714f11 100644 --- a/titles/customizing/master.adoc +++ b/titles/customizing/master.adoc @@ -45,3 +45,5 @@ include::assemblies/assembly-customizing-the-quick-access-card.adoc[leveloffset= include::modules/customizing/proc-customizing-rhdh-metadata-card.adoc[leveloffset=+1] + +include::assemblies/assembly-localization-in-rhdh.adoc[leveloffset=+1]