-
Notifications
You must be signed in to change notification settings - Fork 55
RHIDP-8635-1 - Comprehensive documentation for developers on adding localization support to custom plugins #1443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
19e7af3
9a243fa
d902569
348bc29
2822628
fe7f1d5
a0336d1
31ea439
4a64905
fead28b
0ac0c53
7128efc
9e9d9b6
8a80b35
c27782c
a357528
851d313
863e698
a854520
1d2299b
f80edec
b4acbb6
27f76d4
c2d345e
744683b
a32ba21
0285a12
77316ca
7bc89c6
c7461be
064d75b
91c380a
63b8069
b8fa956
a6cde1f
cac8ecd
b433927
5467d90
fb5e8a4
8d43fec
fab4990
0ed67ed
6d9b857
7e76a5f
7e26249
4afcc34
5550874
959a988
629fd9f
f0cb2d3
eee8f7f
1784af9
ad0722c
4e5f01e
5908549
20814c7
1cdf284
2b31419
a9120d4
3b42cd3
6ef43d3
57f79de
a2be32a
2aa55fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| :_mod-docs-content-type: ASSEMBLY | ||
|
|
||
| [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-overriding-translations.adoc[leveloffset=+2] | ||
|
|
||
| include::modules/customizing-the-appearance/proc-select-rhdh-language.adoc[leveloffset=+1] | ||
|
|
||
| include::modules/customizing-the-appearance/con-language-persistence.adoc[leveloffset=+2] | ||
|
|
||
| == Localization support for custom plugins | ||
|
|
||
| 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] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| :_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`. | ||
|
|
||
| Red Hat Developer Hub 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: | ||
Gerry-Forde marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| [source,yaml,subs="+quotes"] | ||
| ---- | ||
| userSettings: | ||
| persistence: browser # <1> | ||
| ---- | ||
| <1> To opt-out and use browser local storage, set this value to `browser`. Optionally, set this value to `database` to persist across browsers and devices. This the default setting and does not require this configuration to be set. | ||
Gerry-Forde marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,252 @@ | ||||||
| :_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 hardcoded strings with translation calls. For example: | ||||||
Gerry-Forde marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| + | ||||||
| .Before (hardcoded): | ||||||
| [source,json] | ||||||
| ---- | ||||||
| const MyComponent = () => { | ||||||
| return ( | ||||||
| <div> | ||||||
| <h1>My Plugin</h1> | ||||||
| <button>Export CSV</button> | ||||||
| </div> | ||||||
| ); | ||||||
| }; | ||||||
| ---- | ||||||
| + | ||||||
| .After (translated): | ||||||
| [source,json] | ||||||
| ---- | ||||||
| import { useTranslation } from '../hooks/useTranslation'; | ||||||
|
|
||||||
| const MyComponent = () => { | ||||||
| const { t } = useTranslation(); | ||||||
|
|
||||||
| return ( | ||||||
| <div> | ||||||
| <h1>{t('page.title')}</h1> | ||||||
| <button>{t('common.exportCSV')}</button> | ||||||
| </div> | ||||||
| ); | ||||||
| }; | ||||||
| ---- | ||||||
|
|
||||||
| . (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 ( | ||||||
| <div> | ||||||
| <h2>{t(config.titleKey as any)}</h2> | ||||||
| {/* Use 'as any' for dynamic keys */} | ||||||
| </div> | ||||||
| ); | ||||||
| }; | ||||||
| ---- | ||||||
|
|
||||||
| . 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: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
(I think it's dynamic-plugins-default.yaml... I'm not fully sure but I think the file names wouldn't have two periods. Please check this)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @pabel-rh - we do use |
||||||
| + | ||||||
| [source,json] | ||||||
| .`dynamic-plugins.default.yaml` file fragment | ||||||
| ---- | ||||||
| backstage-community.plugin-my-plugin: | ||||||
| translationResources: | ||||||
| - importName: myPluginTranslations | ||||||
| ref: myPluginTranslationRef | ||||||
| module: Alpha | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @debsmita1 in your earlier review, I see you have suggested to add If in case if we go with this change in I recommend, lets skip all this and use export from
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @karthikjeeyar This has been added to the marketplace wrapper's package.json in rhdh
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we have handled this in our plugins via wrappers, but this documentation section talks about how to add localization for the user's custom plugins (which may reside in backstage-community or their own repo).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this document only covers how to add translations to the plugin and not how to integrate them into RHDH, then we can follow the approach used in this package.json: https://github.com/backstage/community-plugins/blob/main/workspaces/linguist/package.json
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @debsmita1 and @karthikjeeyar, could you please confirm what should be updated at line 197?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @debsmita1 I think above mentioned package.json isn't the right link. @Gerry-Forde To export |
||||||
| ---- | ||||||
|
|
||||||
| .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<string, string> { | ||||||
| const flattened: Record<string, string> = {}; | ||||||
| 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... | ||||||
| ---- | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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[] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| :_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. | ||
| ---- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest creating a separate assembly,
Localization Support for Custom Plugins, for these modules, unless we’re fine keeping them in the same assembly. Otherwise, we can address this after the release.”