feat: allow using languages in createDevApp
This allows plugin developers to verify their translations on the fly in dev app. Relates also to #26127 Closes #26161 Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': patch
|
||||
'@backstage/dev-utils': patch
|
||||
---
|
||||
|
||||
Allow using translations in `createDevApp`
|
||||
@@ -16,6 +16,7 @@ import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { SignInProviderConfig } from '@backstage/core-components';
|
||||
import { TranslationResource } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @public
|
||||
export function createDevApp(): DevAppBuilder;
|
||||
@@ -27,6 +28,7 @@ export class DevAppBuilder {
|
||||
addSidebarItem(sidebarItem: JSX.Element): DevAppBuilder;
|
||||
addSignInProvider(provider: SignInProviderConfig): this;
|
||||
addThemes(themes: AppTheme[]): this;
|
||||
addTranslationResource(resource: TranslationResource): this;
|
||||
build(): ComponentType<PropsWithChildren<{}>>;
|
||||
registerApi<
|
||||
Api,
|
||||
@@ -37,6 +39,8 @@ export class DevAppBuilder {
|
||||
>(factory: ApiFactory<Api, Impl, Deps>): DevAppBuilder;
|
||||
registerPlugin(...plugins: BackstagePlugin[]): DevAppBuilder;
|
||||
render(): void;
|
||||
setAvailableLanguages(languages: string[]): this;
|
||||
setDefaultLanguage(language: string): this;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -55,6 +59,9 @@ export const EntityGridItem: (
|
||||
},
|
||||
) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const SidebarLanguageSwitcher: () => React_2.JSX.Element | null;
|
||||
|
||||
// @public
|
||||
export const SidebarSignOutButton: (props: {
|
||||
icon?: IconComponent;
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
import {
|
||||
AppLanguageApi,
|
||||
appLanguageApiRef,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
import { SidebarLanguageSwitcher } from './SidebarLanguageSwitcher';
|
||||
|
||||
describe('SidebarLanguageSwitcher', () => {
|
||||
let languageApi: jest.Mocked<AppLanguageApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
languageApi = {
|
||||
getAvailableLanguages: jest.fn(),
|
||||
getLanguage: jest.fn(),
|
||||
language$: jest.fn(),
|
||||
setLanguage: jest.fn(),
|
||||
};
|
||||
|
||||
languageApi.language$.mockReturnValue(
|
||||
ObservableImpl.of<{ language?: string }>({ language: 'en' }),
|
||||
);
|
||||
languageApi.getLanguage.mockReturnValue({ language: 'en' });
|
||||
languageApi.getAvailableLanguages.mockReturnValue({
|
||||
languages: ['en', 'fi'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should display current language', async () => {
|
||||
const { getByLabelText, getByRole, getByText } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[appLanguageApiRef, languageApi]]}>
|
||||
<SidebarLanguageSwitcher />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
const button = getByLabelText('Language');
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(getByRole('listbox')).toBeInTheDocument();
|
||||
expect(getByText('English')).toBeInTheDocument();
|
||||
expect(getByText('English').parentElement?.parentElement).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
);
|
||||
});
|
||||
|
||||
it('should select different language', async () => {
|
||||
const { getByLabelText, getByRole, getByText } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[appLanguageApiRef, languageApi]]}>
|
||||
<SidebarLanguageSwitcher />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
const button = getByLabelText('Language');
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(getByRole('listbox')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(getByText('suomi'));
|
||||
|
||||
expect(languageApi.setLanguage).toHaveBeenCalledWith('fi');
|
||||
});
|
||||
});
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 React, { useState } from 'react';
|
||||
import { appLanguageApiRef } from '@backstage/core-plugin-api/alpha';
|
||||
import TranslateIcon from '@material-ui/icons/Translate';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import useObservable from 'react-use/esm/useObservable';
|
||||
import { SidebarItem } from '@backstage/core-components';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
/** @public */
|
||||
export const SidebarLanguageSwitcher = () => {
|
||||
const languageApi = useApi(appLanguageApiRef);
|
||||
|
||||
const [languageObservable] = useState(() => languageApi.language$());
|
||||
const { language: currentLanguage } = useObservable(
|
||||
languageObservable,
|
||||
languageApi.getLanguage(),
|
||||
);
|
||||
const [anchorEl, setAnchorEl] = useState<Element | undefined>();
|
||||
|
||||
const { languages } = languageApi.getAvailableLanguages();
|
||||
|
||||
if (languages.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(undefined);
|
||||
};
|
||||
|
||||
const handleOpen = (event: React.MouseEvent) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleSetLanguage = (newLanguage: string | undefined) => {
|
||||
languageApi.setLanguage(newLanguage);
|
||||
setAnchorEl(undefined);
|
||||
};
|
||||
|
||||
const getLanguageDisplayName = (language: string) => {
|
||||
try {
|
||||
const names = new Intl.DisplayNames([language], {
|
||||
type: 'language',
|
||||
});
|
||||
return names.of(language) || language;
|
||||
} catch (err) {
|
||||
return language;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarItem
|
||||
icon={TranslateIcon}
|
||||
text="Language"
|
||||
id="language-button"
|
||||
aria-haspopup="listbox"
|
||||
aria-controls="language-menu"
|
||||
aria-label="switch language"
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={handleOpen}
|
||||
/>
|
||||
<Menu
|
||||
id="language-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'language-button',
|
||||
role: 'listbox',
|
||||
}}
|
||||
>
|
||||
<MenuItem disabled>Choose language</MenuItem>
|
||||
{languages.map(lang => {
|
||||
const active = currentLanguage === lang;
|
||||
return (
|
||||
<MenuItem
|
||||
key={lang}
|
||||
selected={active}
|
||||
aria-selected={active}
|
||||
onClick={() => handleSetLanguage(lang)}
|
||||
>
|
||||
<ListItemText>{getLanguageDisplayName(lang)}</ListItemText>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export { SidebarLanguageSwitcher } from './SidebarLanguageSwitcher';
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export * from './EntityGridItem';
|
||||
export * from './SidebarSignOutButton';
|
||||
export * from './SidebarLanguageSwitcher';
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
IconComponent,
|
||||
RouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { TranslationResource } from '@backstage/core-plugin-api/alpha';
|
||||
import {
|
||||
ScmIntegrationsApi,
|
||||
scmIntegrationsApiRef,
|
||||
@@ -50,7 +51,7 @@ import React, { ComponentType, PropsWithChildren, ReactNode } from 'react';
|
||||
import { createRoutesFromChildren, Route } from 'react-router-dom';
|
||||
import { SidebarThemeSwitcher } from './SidebarThemeSwitcher';
|
||||
import 'react-dom';
|
||||
import { SidebarSignOutButton } from '../components';
|
||||
import { SidebarLanguageSwitcher, SidebarSignOutButton } from '../components';
|
||||
|
||||
let ReactDOMPromise: Promise<
|
||||
typeof import('react-dom') | typeof import('react-dom/client')
|
||||
@@ -98,9 +99,12 @@ export class DevAppBuilder {
|
||||
private readonly routes = new Array<JSX.Element>();
|
||||
private readonly sidebarItems = new Array<JSX.Element>();
|
||||
private readonly signInProviders = new Array<SignInProviderConfig>();
|
||||
private readonly translationResources = new Array<TranslationResource>();
|
||||
|
||||
private defaultPage?: string;
|
||||
private themes?: Array<AppTheme>;
|
||||
private languages?: string[];
|
||||
private defaultLanguage?: string;
|
||||
|
||||
/**
|
||||
* Register one or more plugins to render in the dev app
|
||||
@@ -193,6 +197,30 @@ export class DevAppBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set available languages to be shown in the dev app
|
||||
*/
|
||||
setAvailableLanguages(languages: string[]) {
|
||||
this.languages = languages;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add translation resource to the dev app
|
||||
*/
|
||||
addTranslationResource(resource: TranslationResource) {
|
||||
this.translationResources.push(resource);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default language for the dev app
|
||||
*/
|
||||
setDefaultLanguage(language: string) {
|
||||
this.defaultLanguage = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a DevApp component using the resources registered so far
|
||||
*/
|
||||
@@ -237,6 +265,11 @@ export class DevAppBuilder {
|
||||
bind(plugin.externalRoutes, targets);
|
||||
}
|
||||
},
|
||||
__experimentalTranslations: {
|
||||
defaultLanguage: this.defaultLanguage,
|
||||
availableLanguages: this.languages,
|
||||
resources: this.translationResources,
|
||||
},
|
||||
});
|
||||
|
||||
const DevApp = (
|
||||
@@ -252,6 +285,7 @@ export class DevAppBuilder {
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarThemeSwitcher />
|
||||
<SidebarLanguageSwitcher />
|
||||
<SidebarSignOutButton />
|
||||
</Sidebar>
|
||||
<FlatRoutes>
|
||||
|
||||
Reference in New Issue
Block a user