backstage
backstage copied to clipboard
[RFC] i18n feature
Status: Open for comments
Need
This issue is here to start the discussion about adding i18n support in backstage and see if there is enough interest for it.
Proposal
At first, the goal is to gather interest, collect feedback and then start fleshing out the requirement for an i18n feature.
If there is enough interest, we will need to :
- evaluating potential react library (probably the core team is already more familiar with some)
- deciding how to provide the translations in an app
- ... ?
Alternatives
NA
Risks
- Add complexity, in particular regarding plugin development
- ... ?
Just for awareness, Backstage's membership in CNCF may help: https://www.cncf.io/services-for-projects/#internationalization
Not suggesting this could "do the translation for us", but resources experienced in that area might have guidance/suggestions as well.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Hey @freben, any news about that? I could help to make that happening
No news yet. I can say though, that there's one more angle to this that makes i18n interesting: I'm getting more and more interest from people that want to replace individual little text snippets inside their Backstage instance, not for translation purposes, but for personalization / branding purposes. Sometimes we've added a prop or similar to make them customizable, but an i18n framework properly used will probably greatly help with things like that as well.
Any movement or suggestion is welcome. Maybe even starting with just pointing to commonly used libraries and their strengths/drawbacks
@freben two that I would suggest is
https://www.npmjs.com/package/typesafe-i18n which looks super nice, but some like it is singularly maintained, and the use base is still growing
https://www.npmjs.com/package/i18next This one is a pretty industry standard. A lot of places use it and it has pretty wide range of documented integrations with external systems.
I see pluses and minuses in both directions. i18next just seems like a little better choice as a lot of adopters could leverage their existing tools and processes around localization.
https://github.com/sibelius/ast-i18n
This also looks like an interesting project which could do a fair number of the initial heavy lifting maybe 🤷♂️ Might be worth a pass and a look?
@webark yep looks interesting! At a first glance I'm a bit worried about seemingly generated message IDs, it'll make API stability a bit troublesome
yea. I figured it would be interesting to see what it spit out and if it was helpful
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
We in the Backstage community in Brazil (in general) are very interested in this. 🇧🇷 We can even engage in this process.
Status: Open for comments
My intention was to design a general ideia to provide a first step to use i18n in Backstage.
Need
Backstage is reaching a big audience and not everyone knows english, so the developers maintainers are in necessity for translate the product to provide a better experience.
Also some maintainers want to change some texts in the front-end but not every plugin (or component) has props for that
The ideia is add i18next and support a possibility to translate your plugin.
Proposal
The fist step is provide a simple integration for each plugin add tr
a simple draw from this

First we will update the PluginConfig to enable a new property locale
import {Resource} from "i18next";
export type PluginConfig<
Routes extends AnyRoutes,
ExternalRoutes extends AnyExternalRoutes,
PluginInputOptions extends {},
> = {
id: string;
apis?: Iterable<AnyApiFactory>;
routes?: Routes;
externalRoutes?: ExternalRoutes;
featureFlags?: PluginFeatureFlagConfig[];
__experimentalConfigure?(options?: PluginInputOptions): {};
locale?: Resource
};
This will provide a simple way from the plugin export the keys used inside ( we can provide definitions for that too).
Example:
export constcatalogPlugin= createPlugin({
id: 'catalog',
apis: [
createApiFactory({
api:catalogApiRef,
deps: {
discoveryApi:discoveryApiRef,
fetchApi:fetchApiRef,
},
factory: ({ discoveryApi, fetchApi }) =>
new CatalogClient({ discoveryApi, fetchApi }),
}),
createApiFactory({
api:starredEntitiesApiRef,
deps: { storageApi:storageApiRef},
factory: ({ storageApi }) =>
new DefaultStarredEntitiesApi({ storageApi }),
}),
],
routes: {
catalogIndex:rootRouteRef,
catalogEntity:entityRouteRef,
},
externalRoutes: {
createComponent:createComponentRouteRef,
viewTechDoc:viewTechDocRouteRef,
},
__experimentalConfigure(
options?: CatalogInputPluginOptions,
): CatalogPluginOptions {
const defaultOptions = {
createButtonTitle: 'Create',
};
return { ...defaultOptions, ...options };
},
locale: {
en: {
name: "catalog",
description: "All your software catalog entities"
}
}
});
And could be used like that in the component:
import React from 'react';
import { useTranslation } from 'react-i18next';
export function MyComponent() {
const { t } = useTranslation(["catalog"])
return <p>{t('name')}</p>
}
important: Each component will be using a namespace based in the plugin id
open question: How provide a better devexp here? because in this way the developer needs to remember to use the namespace all the times
With this setup, the backstage could load locale too and override those keys
const app = createApp({
localeConfig: {
lng: "pt-br",
supportedLngs: ['pt', 'en']
resources: {
pt: {
app: {
screeName: "Minha tela",
},
catalog: {
name: "awesome catalog"
}
}
}
},
apis,
plugins: Object.values(plugins),
icons: {
// Custom icon example
alert: AlarmIcon,
},
components: {
SignInPage: props => {
return (
<SignInPage
{...props}
providers={['guest', 'custom', ...providers]}
title="Select a sign-in method"
align="center"
/>
);
},
},
bindRoutes({ bind }) {
bind(catalogPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
viewTechDoc: techdocsPlugin.routes.docRoot,
});
bind(apiDocsPlugin.externalRoutes, {
registerApi: catalogImportPlugin.routes.importPage,
});
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
viewTechDoc: techdocsPlugin.routes.docRoot,
});
bind(orgPlugin.externalRoutes, {
catalogIndex: catalogPlugin.routes.catalogIndex,
});
},
});
Important The locale is following the hierarchy for keys:
language → plugin (namespace) → keys
You don’t need to provide all the keys, the core-app-api will mix those values with the plugin locale.
You can add new languages too to match your expectation over the changes in your app.
The keys you want to use in your app should be provided over the namespace app
localeConfig will accept almost the same configuration i18n.init to add more flexibility
Alternatives
Load from Backend
One interesting alternative is use the Backend system to load all translations. This could be nice to provide a simple way to change locales and a fast update system (like publish the core locales in a CDN)
I didn’t go deep in this approach but we can talk more about that
Use a differrent library
We can choose a different library (like https://github.com/ivanhofer/typesafe-i18n), but I think this will change only details about the implementation, not the whole approach (plugins are locale owner’ s).
My ideia is use i18next because is like a industry standard, so the commuinty don’t really need to learn a new library to start
Risks
How I know this plugin use i18n?
A pain point is how give this information for the developer. The experience could be weird when I have 10 plugins in my language and just one small part only in English.
We can enforce translation, but this could raise the contributing entrypoint
Static load
This solution is very simple and a start point. Because of that, the locale is almost all loadead when the AppManager.getProvider functions is called.
We have some options to add or load translations we could explore
Backend translation
This solution don’t have any use case to translate messages from the backend.
The experience could be weird when your interface is in a language and you receive some messages in English
Locales keys coupled with plugin release
In the current proposal, the keys values are really coupled with the plugin release.
In some cases, to add a new language for the plugin, I will need to release a new version every time.
We can isolate translations in a new module, something like this:
plugins/i18n-translations/src/catalog.ts
export default {
en: {
name: "catalog",
description: "All your software catalog entities"
},
pt: {
name: "catalog",
description: "Todas as suas entidades de catálogo de software"
}
}
And use in each plugin:
import { catalogLocale } from '@backstage/plugin-i18n-translations'
export const catalogPlugin = createPlugin({
id: 'catalog',
// some code
locale: catalogLocale
});
This approach could be nice to move all translations for one place, and we can add some utilities to help in the plugin development (Like a simple way to resolve the namespace for the plugin id)
Add complexity, in particular regarding plugin development
Using i18n is a new thing to be aware when creating a plugin. Over the time, this could bring a high entrypoint to new contributing to Backstage.
Over the time we can simplify this flow, using tools to give a better experience in this process
@Rugvip @freben please take a look in this first ideia, so we can move foward this RFC ;)
Excellent proposal @angeliski!
@angeliski awesome stuff indeed :grin: I think it's well worth moving forward. I think it'd be best if you move your proposal to a separate RFC tbh, and focus the discussion there on the implementation. That could be a draft PRFC too if it ends up making sense.
There are a couple of requirements that I'd like to add in to be considered as part of this:
Must haves
- Dynamic loading of locales
- Any messages loaded as part of the default locale of the plugins must be lazy loaded, preferably in parallel with the plugin itself.
- Any additional locales and overrides must have the option to be lazy loaded.
- Ability to override individual messages in the default locale
- This is a very important use-case for this system, it's a frequent request to for example be able to change the messaging on a single button.
Nice to have
- Type safety and docs for message declarations
- It would be highly desirable to have the available messages be defined as types exported by the plugins, along with documentation that describes the usage of each message.
- Tbh I almost consider this one a must have
- Type safety when using messages in plugin.
- If not already part of i18next we could consider adding a layer for this
- A core locale for common messages.
- This is to avoid duplication, for very common phrases like "Back", "Create", etc.
- I'd say this is slightly lower prio because it's really only there to make supporting new languages easier, it doesn't really work well for the overrides use-case. I'm not sure we actually want this at all tbh.
Nice @Rugvip Next week I will open another RFC to talk only about this implementation and to try to find solutions to those new points
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
@angeliski Do we had another RFC link. I want to know the newest solutions about i18n
Hi! Just wanted to keep an eye on this RFC, also we did some first few steps to this direction in the Playlist plugin https://github.com/backstage/backstage/pull/16955
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Also interested in this; Canada has two official languages so using Backstage in gov would be improved with i18n.
This will also reach into the YAML for us since our services usually have two names and two acronyms. I'm just getting started with Backstage, but this is a quick draft of what I might go with
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: SelfServicePortal
labels:
name-fr: Portail libre-service
acronym-en: SSP
acronym-fr: PLS
---
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: SelfServicePortal-DEV
links:
- title: Home page
url: https://myhost/ssp-pls/en/
icon: Home
type: home-en
- title: Page d'accueil
url: https:/myhost/ssp-pls/fr/
icon: Home
type: home-fr
spec:
type: website
lifecycle: experimental
owner: guests
system: SelfServicePortal
providesApis: [SSP-API]
Some thoughts:
- Usually our stuff is named with both official languages at once "$en-$fr" => "SSP-PLS", but I'm inclined to store this information in a decomposed style
- Name field constraints prevent the usage of diacritics, so having English name as the YAML name is probably best with using labels to store the other langs
metadata.titleandmetadata.descriptionshould probably also have i18n support- Search doesn't seem to consider labels by default, so searching by acronym or name-in-other-language doesn't work out of the box with this approach
noticing now that this approach will need some refinement since labels will also complain about diacritics if they don't even support spaces. Idk but it wouldn't surprise me if this was a k8s limitation as well. Maybe in kubernetes 2 we will finally be able to have emojis as our app selectors.
Policy check failed for system:default/selfserviceportal; caused by Error: "labels.name-fr" is not valid; expected a string that is sequences of [a-zA-Z0-9] separated by any of [-_.], at most 63 characters in total but found "Portail libre-service". To learn more about catalog file format, visit: https://github.com/backstage/backstage/blob/master/docs/architecture-decisions/adr002-default-catalog-file-format.md
Personally, the only constraint on a text field should be the length. Any system that forces me to a-zA-Z0-9-_ would be better off using a GUID as the identifier, and a different field for display name. The docs mention that this is not cool since the GUID changes when the YAML is ingested, but that just seems like a tooling problem?
Maybe our editors should have a shortcut to generate a GUID instead of relying on auto generating one later. This could get messy with references between entities being a little more opaque, but editor hints are nothing new and could be used to give more context to GUIDs everywhere
Pretend that the comments were editor hints
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
uid: a0c049cb-8a9d-4e1f-bb65-37092cacd100
labels:
name-en: Self Service Portal
name-fr: Portail libre-service ééééééééééé
acronym-en: SSP
acronym-fr: PLS
spec:
owner: group:175dc814-a284-423f-b2c1-9eb238369d91 # labels.name-en: Self Service Portal Owners
---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
uid: 801347e3-ad45-41a4-bd9a-9e19bc90bbe3
labels:
name-en: SelfServicePortal-DEV
links:
- title: Home page
url: https://myhost/ssp-pls/en/
icon: Home
type: home-en
- title: Page d'accueil
url: https://myhost/ssp-pls/fr/
icon: Home
type: home-fr
spec:
type: website
lifecycle: experimental
owner: group:8865f809-021b-4fa6-bb36-c5d2048c4076 # labels.name-en: guests
system: a0c049cb-8a9d-4e1f-bb65-37092cacd100 # labels.name-en: Self Service Portal
providesApis:
- 3e0523c7-e60c-41dd-8ba6-561f7679f579 # labels.name-en: Self Service Portal API
Intellisense could auto suggest the guid to tab-replace as you type the name for a thing you want to reference.
Backstage as a language server to provide the intellisense + cross-repo reference validation?
I think I've gotten off track, but hopefully this has been a helpful perspective.
Canada has two official languages so using Backstage in gov would be improved with i18n
Actually, in some provinces (like Quebec) it would be a hard requirement to support both French and English.
Another aspect relative to i18n is also the support for all kind of characters : this is currenlty not the case for tags. See #16278
@Kris-B That's kinda different though. The catalog has adjustable validation rules internally where you control what characters are allowed to go into each field of an entity. So you should be able already to do what you need there, specifically in terms of getting those labels into the catalog storage. Then, as a separate concern, maybe you want to localize the presentation of those labels in the browser? That, indeed, could be targeted by this i18n feature.
@rbideau any issues with this being closed now that the initial implementation (https://github.com/backstage/backstage/pull/17436) for this is in place and will be releases as part of the 1.18.0 release on September 19, 2023?
Many thanks to all those who helped on this, especially @mario-mui! 🚀
Hi @awanlin, I don't use Backstage anymore but I'm glad the project is still active and that the i18n feature has come to fruition.
Feel free to close the issue 👍
Yep this work is steaming ahead - closing this RFC!