Skip to content

Commit e08bcde

Browse files
committed
feat(i18n): add translation support to homepage plugin
Signed-off-by: Rohit Rai <rohitkrai03@gmail.com>
1 parent f91b0cb commit e08bcde

File tree

27 files changed

+825
-61
lines changed

27 files changed

+825
-61
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-dynamic-home-page': minor
3+
---
4+
5+
Add internationalization (i18n) support with German, French, Italian, and Spanish translations.

workspaces/homepage/packages/app/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
TemplateSection,
6464
VisitListener,
6565
HomePageCardMountPoint,
66+
homepageTranslations,
6667
} from '@red-hat-developer-hub/backstage-plugin-dynamic-home-page';
6768

6869
const identityProviders: IdentityProviders = [
@@ -78,6 +79,10 @@ const identityProviders: IdentityProviders = [
7879
const app = createApp({
7980
apis,
8081
themes: getThemes(),
82+
__experimentalTranslations: {
83+
availableLanguages: ['en', 'de', 'fr', 'it', 'es'],
84+
resources: [homepageTranslations],
85+
},
8186
bindRoutes({ bind }) {
8287
bind(catalogPlugin.externalRoutes, {
8388
createComponent: scaffolderPlugin.routes.root,

workspaces/homepage/plugins/dynamic-home-page/dev/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
EntitySection,
6464
TemplateSection,
6565
} from '../src/plugin';
66+
import { homepageTranslations } from '../src/translations';
6667
import { HomePageCardMountPoint, QuickAccessLink } from '../src/types';
6768
import defaultQuickAccess from './quickaccess-default.json';
6869

@@ -276,6 +277,9 @@ const createPage = ({
276277

277278
createDevApp()
278279
.registerPlugin(dynamicHomePagePlugin)
280+
.addTranslationResource(homepageTranslations)
281+
.setAvailableLanguages(['en', 'de', 'fr', 'it', 'es'])
282+
.setDefaultLanguage('en')
279283
.addThemes(getAllThemes())
280284
.addPage(
281285
createPage({

workspaces/homepage/plugins/dynamic-home-page/report.api.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { FeaturedDocsCardProps } from '@backstage/plugin-home';
1111
import { JSX as JSX_2 } from 'react/jsx-runtime';
1212
import { RouteRef } from '@backstage/core-plugin-api';
1313
import { StarredEntitiesProps } from '@backstage/plugin-home';
14+
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
15+
import { TranslationResource } from '@backstage/core-plugin-api/alpha';
1416
import { VisitedByTypeProps } from '@backstage/plugin-home';
1517

1618
// @public (undocumented)
@@ -112,6 +114,54 @@ export interface HomePageCardMountPointConfig {
112114
priority?: number;
113115
}
114116

117+
// @public
118+
export const homepageTranslationRef: TranslationRef<"plugin.homepage", {
119+
readonly "search.placeholder": string;
120+
readonly "header.local": string;
121+
readonly "header.welcome": string;
122+
readonly "header.welcomePersonalized": string;
123+
readonly "templates.error": string;
124+
readonly "templates.title": string;
125+
readonly "templates.empty": string;
126+
readonly "templates.register": string;
127+
readonly "templates.fetchError": string;
128+
readonly "templates.emptyDescription": string;
129+
readonly "templates.viewAll": string;
130+
readonly "entities.error": string;
131+
readonly "entities.title": string;
132+
readonly "entities.close": string;
133+
readonly "entities.description": string;
134+
readonly "entities.empty": string;
135+
readonly "entities.register": string;
136+
readonly "entities.fetchError": string;
137+
readonly "entities.emptyDescription": string;
138+
readonly "entities.viewAll": string;
139+
readonly "homePage.empty": string;
140+
readonly "quickAccess.error": string;
141+
readonly "quickAccess.title": string;
142+
readonly "quickAccess.fetchError": string;
143+
readonly "featuredDocs.learnMore": string;
144+
readonly "onboarding.guest": string;
145+
readonly "onboarding.greeting.goodMorning": string;
146+
readonly "onboarding.greeting.goodAfternoon": string;
147+
readonly "onboarding.greeting.goodEvening": string;
148+
readonly "onboarding.getStarted.title": string;
149+
readonly "onboarding.getStarted.ariaLabel": string;
150+
readonly "onboarding.getStarted.description": string;
151+
readonly "onboarding.getStarted.buttonText": string;
152+
readonly "onboarding.explore.title": string;
153+
readonly "onboarding.explore.ariaLabel": string;
154+
readonly "onboarding.explore.description": string;
155+
readonly "onboarding.explore.buttonText": string;
156+
readonly "onboarding.learn.title": string;
157+
readonly "onboarding.learn.ariaLabel": string;
158+
readonly "onboarding.learn.description": string;
159+
readonly "onboarding.learn.buttonText": string;
160+
}>;
161+
162+
// @public
163+
export const homepageTranslations: TranslationResource<"plugin.homepage">;
164+
115165
// @public (undocumented)
116166
export const JokeCard: ComponentType<{
117167
defaultCategory?: 'any' | 'programming';

workspaces/homepage/plugins/dynamic-home-page/src/components/CustomizableHomePage.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useMemo } from 'react';
1919
import { Content, EmptyState, Page } from '@backstage/core-components';
2020

2121
import { HomePageCardMountPoint } from '../types';
22+
import { useTranslation } from '../hooks/useTranslation';
2223

2324
import { Header, HeaderProps } from './Header';
2425
import { CustomizableGrid } from './CustomizableGrid';
@@ -28,6 +29,7 @@ export interface HomePageProps extends HeaderProps {
2829
}
2930

3031
export const CustomizableHomePage = (props: HomePageProps) => {
32+
const { t } = useTranslation();
3133
const filteredAndSortedHomePageCards = useMemo(() => {
3234
if (!props.cards) {
3335
return [];
@@ -51,10 +53,7 @@ export const CustomizableHomePage = (props: HomePageProps) => {
5153
<Header {...props} />
5254
<Content>
5355
{filteredAndSortedHomePageCards.length === 0 ? (
54-
<EmptyState
55-
title="No home page cards (mount points) configured or found."
56-
missing="content"
57-
/>
56+
<EmptyState title={t('homePage.empty')} missing="content" />
5857
) : (
5958
<CustomizableGrid mountPoints={filteredAndSortedHomePageCards} />
6059
)}

workspaces/homepage/plugins/dynamic-home-page/src/components/EntitySection/EntitySection.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import {
4444
addDismissedEntityIllustrationUsers,
4545
hasEntityIllustrationUserDismissed,
4646
} from '../../utils/utils';
47+
import { useTranslation } from '../../hooks/useTranslation';
48+
import { Trans } from '../Trans';
4749

4850
const StyledLink = styled(BackstageLink)(({ theme }) => ({
4951
textDecoration: 'none',
@@ -56,6 +58,7 @@ const StyledLink = styled(BackstageLink)(({ theme }) => ({
5658

5759
export const EntitySection = () => {
5860
const theme = useTheme();
61+
const { t } = useTranslation();
5962
const { displayName, loading: profileLoading } = useUserProfile();
6063
const [isRemoveFirstCard, setIsRemoveFirstCard] = useState(false);
6164
const [showDiscoveryCard, setShowDiscoveryCard] = useState(true);
@@ -109,10 +112,10 @@ export const EntitySection = () => {
109112
);
110113
} else if (!data) {
111114
content = (
112-
<WarningPanel severity="error" title="Could not fetch data.">
115+
<WarningPanel severity="error" title={t('entities.fetchError')}>
113116
<CodeSnippet
114117
language="text"
115-
text={error?.toString() ?? 'Unknown error'}
118+
text={error?.toString() ?? t('entities.error')}
116119
/>
117120
</WarningPanel>
118121
);
@@ -165,14 +168,13 @@ export const EntitySection = () => {
165168
<Box sx={{ p: 2 }}>
166169
<Box>
167170
<Typography variant="body2" paragraph>
168-
Browse the Systems, Components, Resources, and APIs that
169-
are available in your organization.
171+
{t('entities.description')}
170172
</Typography>
171173
</Box>
172174
{entities?.length > 0 && (
173175
<IconButton
174176
onClick={handleClose}
175-
aria-label="close"
177+
aria-label={t('entities.close')}
176178
style={{
177179
position: 'absolute',
178180
top: '8px',
@@ -224,7 +226,7 @@ export const EntitySection = () => {
224226
>
225227
<CardContent>
226228
<Typography sx={{ fontSize: '1.125rem', fontWeight: 500 }}>
227-
No software catalog added yet
229+
{t('entities.empty')}
228230
</Typography>
229231
<Typography
230232
sx={{
@@ -234,11 +236,10 @@ export const EntitySection = () => {
234236
mb: '16px',
235237
}}
236238
>
237-
Once software catalogs are added, this space will showcase
238-
relevant content tailored to your experience.
239+
{t('entities.emptyDescription')}
239240
</Typography>
240241
<StyledLink to="/catalog-import" underline="none">
241-
Register a component
242+
{t('entities.register')}
242243
</StyledLink>
243244
</CardContent>
244245
</Box>
@@ -268,13 +269,16 @@ export const EntitySection = () => {
268269
fontSize: '1.5rem',
269270
}}
270271
>
271-
Explore Your Software Catalog
272+
{t('entities.title')}
272273
</Typography>
273274
{content}
274275
{entities?.length > 0 && (
275276
<Box sx={{ pt: 2 }}>
276277
<ViewMoreLink to="/catalog">
277-
View all {data?.totalItems ? data?.totalItems : ''} catalog entities
278+
<Trans
279+
message="entities.viewAll"
280+
params={{ count: data?.totalItems?.toString() || '' }}
281+
/>
278282
</ViewMoreLink>
279283
</Box>
280284
)}

workspaces/homepage/plugins/dynamic-home-page/src/components/FeaturedDocsCard.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
FeaturedDocsCardProps,
2020
} from '@backstage/plugin-home';
2121

22+
import { useTranslation } from '../hooks/useTranslation';
23+
2224
/**
2325
* Overrides `FeaturedDocsCard` from the home plugin, but overrides the
2426
* `subLinkText` prop to be " Learn more" instead of "LEARN MORE".
@@ -27,5 +29,11 @@ import {
2729
* 2. To add a small missing gap between the title and the button
2830
*/
2931
export const FeaturedDocsCard = (props: FeaturedDocsCardProps) => {
30-
return <PluginHomeFeaturedDocsCard subLinkText=" Learn more" {...props} />;
32+
const { t } = useTranslation();
33+
return (
34+
<PluginHomeFeaturedDocsCard
35+
subLinkText={` ${t('featuredDocs.learnMore')}`}
36+
{...props}
37+
/>
38+
);
3139
};

workspaces/homepage/plugins/dynamic-home-page/src/components/Header.tsx

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { ClockConfig, HeaderWorldClock } from '@backstage/plugin-home';
2323
import useAsync from 'react-use/esm/useAsync';
2424

2525
import { LocalClock, LocalClockProps } from './LocalClock';
26+
import { useTranslation } from '../hooks/useTranslation';
2627

2728
export interface HeaderProps {
2829
title?: string;
@@ -44,35 +45,68 @@ export interface HeaderProps {
4445
// return 'Good evening {{firstName}}';
4546
// };
4647

47-
export const getPersonalizedTitle = (
48+
/**
49+
* Handles user-provided title customization with variable interpolation.
50+
*
51+
* When users provide custom titles in their configuration, they take responsibility
52+
* for internationalization at the configuration level. This function simply handles
53+
* the variable interpolation for their custom strings.
54+
*
55+
* Supports variables:
56+
* - {{firstName}} - First part of displayName (split by space)
57+
* - {{displayName}} - Full displayName from user profile
58+
*
59+
* @param title - User-provided title string with optional {{variables}}
60+
* @param displayName - User's displayName from profile, if available
61+
*/
62+
const interpolateUserTitle = (
4863
title: string,
4964
displayName: string | undefined,
5065
) => {
51-
const firstName = displayName?.split(' ')[0];
52-
const replacedTitle = title
53-
.replace('{{firstName}}', firstName ?? '')
54-
.replace('{{displayName}}', displayName ?? '');
55-
return replacedTitle;
66+
if (!displayName) {
67+
// Remove variable placeholders when no displayName is available
68+
return title.replace(/\{\{(firstName|displayName)\}\}/g, '');
69+
}
70+
71+
const firstName = displayName.split(' ')[0];
72+
return title
73+
.replace(/\{\{firstName\}\}/g, firstName)
74+
.replace(/\{\{displayName\}\}/g, displayName);
5675
};
5776

5877
export const Header = (props: HeaderProps) => {
5978
const identityApi = useApi(identityApiRef);
6079
const { value: profile } = useAsync(() => identityApi.getProfileInfo());
80+
const { t } = useTranslation();
6181

6282
const title = useMemo<string>(() => {
83+
const firstName = profile?.displayName?.split(' ')[0];
84+
85+
// Priority 1: User-provided personalized title (user handles i18n)
6386
if (profile?.displayName && props.personalizedTitle) {
64-
return getPersonalizedTitle(props.personalizedTitle, profile.displayName);
65-
} else if (props.title) {
66-
return getPersonalizedTitle(props.title, profile?.displayName);
87+
return interpolateUserTitle(props.personalizedTitle, profile.displayName);
6788
}
68-
// return getPersonalizedTitle(getTimeBasedTitle(), profile?.displayName);
69-
return getPersonalizedTitle('Welcome back!', profile?.displayName);
70-
}, [profile?.displayName, props.personalizedTitle, props.title]);
89+
90+
// Priority 2: User-provided general title (user handles i18n)
91+
if (props.title) {
92+
return interpolateUserTitle(props.title, profile?.displayName);
93+
}
94+
95+
// Default: Use plugin's translation system
96+
if (profile?.displayName && firstName) {
97+
return t('header.welcomePersonalized' as any, { name: firstName });
98+
}
99+
100+
return t('header.welcome');
101+
}, [profile?.displayName, props.personalizedTitle, props.title, t]);
71102

72103
const subtitle = useMemo<string | undefined>(() => {
73-
return props.subtitle
74-
? getPersonalizedTitle(props.subtitle, profile?.displayName)
75-
: undefined;
104+
// User-provided subtitle (user handles i18n)
105+
if (props.subtitle) {
106+
return interpolateUserTitle(props.subtitle, profile?.displayName);
107+
}
108+
109+
return undefined;
76110
}, [props.subtitle, profile?.displayName]);
77111

78112
return (
@@ -86,7 +120,7 @@ export const Header = (props: HeaderProps) => {
86120
label={
87121
props.localClock?.label ??
88122
(props.worldClocks && props.worldClocks.length > 0
89-
? 'Local'
123+
? t('header.local')
90124
: undefined)
91125
}
92126
format={

workspaces/homepage/plugins/dynamic-home-page/src/components/HomePage.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useMemo } from 'react';
1919
import { Content, EmptyState, Page } from '@backstage/core-components';
2020

2121
import { HomePageCardMountPoint } from '../types';
22+
import { useTranslation } from '../hooks/useTranslation';
2223

2324
import { Header, HeaderProps } from './Header';
2425
import { ReadOnlyGrid } from './ReadOnlyGrid';
@@ -28,6 +29,7 @@ export interface HomePageProps extends HeaderProps {
2829
}
2930

3031
export const HomePage = (props: HomePageProps) => {
32+
const { t } = useTranslation();
3133
const filteredAndSortedHomePageCards = useMemo(() => {
3234
if (!props.cards) {
3335
return [];
@@ -48,13 +50,10 @@ export const HomePage = (props: HomePageProps) => {
4850

4951
return (
5052
<Page themeId="home">
51-
<Header title="Welcome back!" {...props} />
53+
<Header title={t('header.welcome')} {...props} />
5254
<Content>
5355
{filteredAndSortedHomePageCards.length === 0 ? (
54-
<EmptyState
55-
title="No home page cards (mount points) configured or found."
56-
missing="content"
57-
/>
56+
<EmptyState title={t('homePage.empty')} missing="content" />
5857
) : (
5958
<ReadOnlyGrid mountPoints={filteredAndSortedHomePageCards} />
6059
)}

0 commit comments

Comments
 (0)