feat: Introduced experimental support for internationalization.
Signed-off-by: rui ma <ruima@alauda.io>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': minor
|
||||
'@backstage/core-app-api': minor
|
||||
'@backstage/plugin-user-settings': patch
|
||||
---
|
||||
|
||||
Introduced experimental support for internationalization.
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
id: internationalization
|
||||
title: Internationalization (Experimental)
|
||||
description: Documentation on adding internationalization to the plugin
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Backstage core function provides internationalization for plugins
|
||||
|
||||
## For a plugin developer
|
||||
|
||||
When you are creating your plugin, you have the possibility to use `createTranslationRef` to define all messages for your plugin. For example
|
||||
|
||||
```typescript jsx
|
||||
import { createTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export const userSettingsTranslationRef = createTranslationRef({
|
||||
id: 'user-settings',
|
||||
messages: {
|
||||
language: 'Language',
|
||||
change_the_language: 'Change the language',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
And the using this messages in your components like:
|
||||
|
||||
```typescript jsx
|
||||
const t = useTranslationRef(userSettingsTranslationRef);
|
||||
|
||||
return (
|
||||
<ListItemText
|
||||
className={classes.listItemText}
|
||||
primary={t('language')}
|
||||
secondary={t('change_the_language')}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
## For an application developer overwrite plugin messages
|
||||
|
||||
```diff typescript jsx
|
||||
const app = createApp({
|
||||
+ __experimentalI18n: {
|
||||
+ supportedLanguages: ['zh', 'en'],
|
||||
+ messages: [
|
||||
+ createTranslationResource({
|
||||
+ ref: userSettingsTranslationRef,
|
||||
+ messages: {
|
||||
+ zh: {
|
||||
+ select_lng: '选择中文-app',
|
||||
+ },
|
||||
+ },
|
||||
+ }),
|
||||
+ ],
|
||||
+ },
|
||||
...
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
## API Report File for "@backstage/core-app-api"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AppTranslationApi } from '@backstage/core-plugin-api/alpha';
|
||||
import { i18n } from 'i18next';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export class AppTranslationApiImpl implements AppTranslationApi {
|
||||
// (undocumented)
|
||||
addLazyResources<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
initResources?: Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: TranslationMessages<TranslationRef>;
|
||||
}>
|
||||
>,
|
||||
): void;
|
||||
// (undocumented)
|
||||
addResources<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
initResources?: TranslationMessages<TranslationRef<Messages>>,
|
||||
): void;
|
||||
// (undocumented)
|
||||
addResourcesByRef<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
): void;
|
||||
// (undocumented)
|
||||
static create(options?: ExperimentalI18n): AppTranslationApiImpl;
|
||||
// (undocumented)
|
||||
getI18n(): i18n;
|
||||
// (undocumented)
|
||||
initMessages(options?: ExperimentalI18n): void;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export function createTranslationResource<T extends TranslationRef>(options: {
|
||||
ref: T;
|
||||
messages?: TranslationMessages<T>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: TranslationMessages<T>;
|
||||
}>
|
||||
>;
|
||||
}): {
|
||||
ref: T;
|
||||
messages?: TranslationMessages<T> | undefined;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: TranslationMessages<T>;
|
||||
}>
|
||||
>;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type ExperimentalI18n = {
|
||||
supportedLanguages: string[];
|
||||
fallbackLanguage?: string | string[];
|
||||
messages?: Array<{
|
||||
ref: TranslationRef;
|
||||
messages?: TranslationMessages<TranslationRef>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: TranslationMessages<TranslationRef>;
|
||||
}>
|
||||
>;
|
||||
}>;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type TranslationMessages<T> = T extends TranslationRef<infer R>
|
||||
? Record<string, Partial<R>>
|
||||
: never;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -64,6 +64,7 @@ import { SessionState } from '@backstage/core-plugin-api';
|
||||
import { StorageApi } from '@backstage/core-plugin-api';
|
||||
import { StorageValueSnapshot } from '@backstage/core-plugin-api';
|
||||
import { SubRouteRef } from '@backstage/core-plugin-api';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @public
|
||||
export class AlertApiForwarder implements AlertApi {
|
||||
@@ -220,6 +221,20 @@ export type AppOptions = {
|
||||
themes: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
|
||||
configLoader?: AppConfigLoader;
|
||||
bindRoutes?(context: { bind: AppRouteBinder }): void;
|
||||
__experimentalI18n?: {
|
||||
supportedLanguages: string[];
|
||||
fallbackLanguage?: string | string[];
|
||||
messages?: Array<{
|
||||
ref: TranslationRef;
|
||||
messages?: TranslationMessages<TranslationRef>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: TranslationMessages<TranslationRef>;
|
||||
}>
|
||||
>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
@@ -3,9 +3,22 @@
|
||||
"description": "Core app API used by Backstage apps",
|
||||
"version": "1.10.0-next.1",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"alpha": [
|
||||
"src/alpha.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"backstage": {
|
||||
"role": "web-library"
|
||||
@@ -39,7 +52,10 @@
|
||||
"@types/prop-types": "^15.7.3",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"history": "^5.0.0",
|
||||
"i18next": "^22.4.15",
|
||||
"i18next-browser-languagedetector": "^7.0.2",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-use": "^17.2.4",
|
||||
"zen-observable": "^0.10.0",
|
||||
"zod": "^3.21.4"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 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 * from './apis/implementations/AppTranslationApi';
|
||||
export * from './app/TranslationResource';
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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 { createTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { AppTranslationApiImpl } from './AppTranslationImpl';
|
||||
import i18next from 'i18next';
|
||||
|
||||
jest.mock('i18next', () => ({
|
||||
createInstance: jest.fn(() => ({
|
||||
use: jest.fn(() => ({
|
||||
init: jest.fn(),
|
||||
use: jest.fn(),
|
||||
addResourceBundle: jest.fn(),
|
||||
reloadResources: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
services: {
|
||||
languageUtils: {
|
||||
getFallbackCodes: jest.fn().mockReturnValue(['en']),
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fallbackLng: 'en',
|
||||
},
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('AppTranslationApiImpl', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create i18n instance and init with options', () => {
|
||||
const i18nMock = i18next.createInstance() as any;
|
||||
const useReturnMock = i18nMock.use();
|
||||
jest.spyOn(i18nMock, 'use').mockReturnValue(useReturnMock);
|
||||
jest.spyOn(i18next, 'createInstance').mockReturnValue(i18nMock);
|
||||
|
||||
const instance = AppTranslationApiImpl.create({
|
||||
supportedLanguages: ['en', 'zh'],
|
||||
});
|
||||
|
||||
expect(i18next.createInstance).toHaveBeenCalled();
|
||||
expect(i18nMock.use).toHaveBeenCalled();
|
||||
expect(useReturnMock.init).toHaveBeenCalledWith({
|
||||
fallbackLng: 'en',
|
||||
supportedLngs: ['en', 'zh'],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
bindI18n: 'loaded languageChanged',
|
||||
},
|
||||
});
|
||||
expect(instance).toBeInstanceOf(AppTranslationApiImpl);
|
||||
});
|
||||
|
||||
it('should init messages correctly', () => {
|
||||
const useResourcesMock = jest.spyOn(
|
||||
AppTranslationApiImpl.prototype,
|
||||
'addResources',
|
||||
);
|
||||
const useLazyResourcesMock = jest.spyOn(
|
||||
AppTranslationApiImpl.prototype,
|
||||
'addLazyResources',
|
||||
);
|
||||
const ref = createTranslationRef({
|
||||
id: 'ref-id',
|
||||
messages: {
|
||||
key1: '',
|
||||
},
|
||||
});
|
||||
|
||||
const options = {
|
||||
supportedLanguages: ['en'],
|
||||
messages: [
|
||||
{
|
||||
ref,
|
||||
messages: {
|
||||
en: { key1: 'value1' },
|
||||
},
|
||||
lazyMessages: {
|
||||
en: () => Promise.resolve({ key2: 'value2' }),
|
||||
} as any,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const instance = AppTranslationApiImpl.create({
|
||||
supportedLanguages: ['en'],
|
||||
});
|
||||
instance.initMessages(options);
|
||||
|
||||
expect(useResourcesMock).toHaveBeenCalledWith(
|
||||
options.messages[0].ref,
|
||||
options.messages[0].messages,
|
||||
);
|
||||
expect(useLazyResourcesMock).toHaveBeenCalledWith(
|
||||
options.messages[0].ref,
|
||||
options.messages[0].lazyMessages,
|
||||
);
|
||||
});
|
||||
|
||||
it('should useResources correctly', () => {
|
||||
const i18nMock = i18next.createInstance() as any;
|
||||
const useReturnMock = i18nMock.use();
|
||||
jest.spyOn(i18nMock, 'use').mockReturnValue(useReturnMock);
|
||||
jest.spyOn(i18next, 'createInstance').mockReturnValue(i18nMock);
|
||||
|
||||
const ref = createTranslationRef({
|
||||
id: 'ref-id',
|
||||
messages: {
|
||||
key1: 'value1',
|
||||
},
|
||||
resources: {
|
||||
en: {
|
||||
key1: 'value2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const instance = AppTranslationApiImpl.create({
|
||||
supportedLanguages: ['en'],
|
||||
});
|
||||
instance.addResources(ref);
|
||||
|
||||
expect(useReturnMock.addResourceBundle).toHaveBeenCalledWith(
|
||||
'en',
|
||||
'ref-id',
|
||||
{ key1: 'value2' },
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should useLazyResources correctly', () => {
|
||||
const i18nMock = i18next.createInstance() as any;
|
||||
const useReturnMock = i18nMock.use();
|
||||
|
||||
jest.spyOn(i18nMock, 'use').mockReturnValue(useReturnMock);
|
||||
jest.spyOn(i18next, 'createInstance').mockReturnValue(i18nMock);
|
||||
|
||||
const ref = createTranslationRef({
|
||||
id: 'ref-id',
|
||||
messages: {
|
||||
key1: 'value1',
|
||||
},
|
||||
lazyResources: {
|
||||
en: () => Promise.resolve({ messages: { key1: 'value2' } }),
|
||||
},
|
||||
});
|
||||
|
||||
const instance = AppTranslationApiImpl.create({
|
||||
supportedLanguages: ['en'],
|
||||
});
|
||||
instance.addLazyResources(ref);
|
||||
setTimeout(() => {
|
||||
expect(useReturnMock.addResourceBundle).toHaveBeenCalledWith(
|
||||
'en',
|
||||
'ref-id',
|
||||
{ key1: 'value2' },
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2023 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 {
|
||||
AppTranslationApi,
|
||||
TranslationRef,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
import i18next, { type i18n } from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import { TranslationMessages } from '../../../alpha';
|
||||
|
||||
/** @alpha */
|
||||
export type ExperimentalI18n = {
|
||||
supportedLanguages: string[];
|
||||
fallbackLanguage?: string | string[];
|
||||
messages?: Array<{
|
||||
ref: TranslationRef;
|
||||
messages?: TranslationMessages<TranslationRef>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{ messages: TranslationMessages<TranslationRef> }>
|
||||
>;
|
||||
}>;
|
||||
};
|
||||
|
||||
/** @alpha */
|
||||
export class AppTranslationApiImpl implements AppTranslationApi {
|
||||
static create(options?: ExperimentalI18n) {
|
||||
const i18n = i18next.createInstance().use(initReactI18next);
|
||||
|
||||
i18n.use(LanguageDetector);
|
||||
|
||||
i18n.init({
|
||||
fallbackLng: options?.fallbackLanguage || 'en',
|
||||
supportedLngs: options?.supportedLanguages || ['en'],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
bindI18n: 'loaded languageChanged',
|
||||
},
|
||||
});
|
||||
|
||||
return new AppTranslationApiImpl(i18n, options);
|
||||
}
|
||||
|
||||
private readonly cache = new WeakSet<TranslationRef>();
|
||||
private readonly lazyCache = new WeakMap<TranslationRef, Set<string>>();
|
||||
|
||||
getI18n() {
|
||||
return this.i18n;
|
||||
}
|
||||
|
||||
initMessages(options?: ExperimentalI18n) {
|
||||
if (options?.messages?.length) {
|
||||
options.messages.forEach(appMessage => {
|
||||
if (appMessage.messages) {
|
||||
this.addResources(appMessage.ref, appMessage.messages);
|
||||
}
|
||||
|
||||
if (appMessage.lazyMessages) {
|
||||
this.addLazyResources(appMessage.ref, appMessage.lazyMessages);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addResourcesByRef<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
): void {
|
||||
this.addResources(translationRef);
|
||||
this.addLazyResources(translationRef);
|
||||
}
|
||||
|
||||
addResources<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
initResources?: TranslationMessages<TranslationRef<Messages>>,
|
||||
) {
|
||||
const resources = initResources || translationRef.getResources();
|
||||
if (!resources || this.cache.has(translationRef)) {
|
||||
return;
|
||||
}
|
||||
this.cache.add(translationRef);
|
||||
Object.entries(resources).forEach(([language, messages]) => {
|
||||
this.i18n.addResourceBundle(
|
||||
language,
|
||||
translationRef.getId(),
|
||||
messages,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
addLazyResources<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
initResources?: Record<
|
||||
string,
|
||||
() => Promise<{ messages: TranslationMessages<TranslationRef> }>
|
||||
>,
|
||||
) {
|
||||
let cache = this.lazyCache.get(translationRef);
|
||||
|
||||
if (!cache) {
|
||||
cache = new Set();
|
||||
this.lazyCache.set(translationRef, cache);
|
||||
}
|
||||
|
||||
const {
|
||||
language: currentLanguage,
|
||||
services,
|
||||
options,
|
||||
addResourceBundle,
|
||||
reloadResources,
|
||||
} = this.i18n;
|
||||
|
||||
if (cache.has(currentLanguage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const namespace = translationRef.getId();
|
||||
const lazyResources = initResources || translationRef.getLazyResources();
|
||||
|
||||
const fallbackLanguages = services.languageUtils.getFallbackCodes(
|
||||
options.fallbackLng,
|
||||
currentLanguage,
|
||||
) as string[];
|
||||
|
||||
Promise.allSettled(
|
||||
[...fallbackLanguages, currentLanguage].map(addLanguage),
|
||||
).then(results => {
|
||||
if (results.some(result => result.status === 'fulfilled')) {
|
||||
this.i18n.emit('loaded');
|
||||
}
|
||||
});
|
||||
|
||||
async function addLanguage(language: string) {
|
||||
if (cache!.has(language)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache!.add(language);
|
||||
|
||||
let loadBackend: Promise<void> | undefined;
|
||||
|
||||
if (services.backendConnector?.backend) {
|
||||
loadBackend = reloadResources([language], [namespace]);
|
||||
}
|
||||
|
||||
const loadLazyResources = lazyResources?.[language];
|
||||
|
||||
if (!loadLazyResources) {
|
||||
await loadBackend;
|
||||
return;
|
||||
}
|
||||
|
||||
const [result] = await Promise.allSettled([
|
||||
loadLazyResources(),
|
||||
loadBackend,
|
||||
]);
|
||||
|
||||
if (result.status === 'rejected') {
|
||||
throw result.reason;
|
||||
}
|
||||
|
||||
addResourceBundle(
|
||||
language,
|
||||
namespace,
|
||||
result.value.messages,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(private readonly i18n: i18n, options?: ExperimentalI18n) {
|
||||
this.initMessages(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 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 * from './AppTranslationImpl';
|
||||
@@ -260,6 +260,46 @@ describe('Integration Test', () => {
|
||||
expect(screen.getByText('extLink4: <none>')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('runs success with __experimentalI18n', async () => {
|
||||
const app = new AppManager({
|
||||
apis: [noOpAnalyticsApi],
|
||||
defaultApis: [],
|
||||
themes,
|
||||
icons,
|
||||
plugins: [],
|
||||
components,
|
||||
configLoader: async () => [],
|
||||
bindRoutes: ({ bind }) => {
|
||||
bind(plugin1.externalRoutes, {
|
||||
extRouteRef1: plugin1RouteRef,
|
||||
extRouteRef2: plugin2RouteRef,
|
||||
});
|
||||
},
|
||||
__experimentalI18n: {
|
||||
supportedLanguages: ['en'],
|
||||
},
|
||||
});
|
||||
|
||||
const Provider = app.getProvider();
|
||||
const Router = app.getRouter();
|
||||
|
||||
await renderWithEffects(
|
||||
<Provider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<ExposedComponent />} />
|
||||
<Route path="/foo" element={<HiddenComponent />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('extLink1: /')).toBeInTheDocument();
|
||||
expect(screen.getByText('extLink2: /foo')).toBeInTheDocument();
|
||||
expect(screen.getByText('extLink3: <none>')).toBeInTheDocument();
|
||||
expect(screen.getByText('extLink4: <none>')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should wait for the config to load before calling feature flags', async () => {
|
||||
const storageFlags = new LocalStorageFeatureFlags();
|
||||
jest.spyOn(storageFlags, 'registerFlag');
|
||||
|
||||
@@ -42,6 +42,10 @@ import {
|
||||
BackstagePlugin,
|
||||
FeatureFlag,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
AppTranslationApi,
|
||||
appTranslationApiRef,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
import { ApiFactoryRegistry, ApiResolver } from '../apis/system';
|
||||
import {
|
||||
childDiscoverer,
|
||||
@@ -75,6 +79,8 @@ import { resolveRouteBindings } from './resolveRouteBindings';
|
||||
import { isReactRouterBeta } from './isReactRouterBeta';
|
||||
import { InternalAppContext } from './InternalAppContext';
|
||||
import { AppRouter, getBasePath } from './AppRouter';
|
||||
import { AppTranslationProvider } from './AppTranslationProvider';
|
||||
import { AppTranslationApiImpl } from '../apis/implementations/AppTranslationApi';
|
||||
|
||||
type CompatiblePlugin =
|
||||
| BackstagePlugin
|
||||
@@ -209,6 +215,7 @@ export class AppManager implements BackstageApp {
|
||||
private readonly configLoader?: AppConfigLoader;
|
||||
private readonly defaultApis: Iterable<AnyApiFactory>;
|
||||
private readonly bindRoutes: AppOptions['bindRoutes'];
|
||||
private readonly appTranslationApi: AppTranslationApi;
|
||||
|
||||
private readonly appIdentityProxy = new AppIdentityProxy();
|
||||
private readonly apiFactoryRegistry: ApiFactoryRegistry;
|
||||
@@ -224,6 +231,9 @@ export class AppManager implements BackstageApp {
|
||||
this.defaultApis = options.defaultApis ?? [];
|
||||
this.bindRoutes = options.bindRoutes;
|
||||
this.apiFactoryRegistry = new ApiFactoryRegistry();
|
||||
this.appTranslationApi = AppTranslationApiImpl.create(
|
||||
options.__experimentalI18n,
|
||||
);
|
||||
}
|
||||
|
||||
getPlugins(): BackstagePlugin[] {
|
||||
@@ -379,24 +389,26 @@ export class AppManager implements BackstageApp {
|
||||
return (
|
||||
<ApiProvider apis={this.getApiHolder()}>
|
||||
<AppContextProvider appContext={appContext}>
|
||||
<ThemeProvider>
|
||||
<RoutingProvider
|
||||
routePaths={routing.paths}
|
||||
routeParents={routing.parents}
|
||||
routeObjects={routing.objects}
|
||||
routeBindings={routeBindings}
|
||||
basePath={getBasePath(loadedConfig.api)}
|
||||
>
|
||||
<InternalAppContext.Provider
|
||||
value={{
|
||||
routeObjects: routing.objects,
|
||||
appIdentityProxy: this.appIdentityProxy,
|
||||
}}
|
||||
<AppTranslationProvider>
|
||||
<ThemeProvider>
|
||||
<RoutingProvider
|
||||
routePaths={routing.paths}
|
||||
routeParents={routing.parents}
|
||||
routeObjects={routing.objects}
|
||||
routeBindings={routeBindings}
|
||||
basePath={getBasePath(loadedConfig.api)}
|
||||
>
|
||||
{children}
|
||||
</InternalAppContext.Provider>
|
||||
</RoutingProvider>
|
||||
</ThemeProvider>
|
||||
<InternalAppContext.Provider
|
||||
value={{
|
||||
routeObjects: routing.objects,
|
||||
appIdentityProxy: this.appIdentityProxy,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</InternalAppContext.Provider>
|
||||
</RoutingProvider>
|
||||
</ThemeProvider>
|
||||
</AppTranslationProvider>
|
||||
</AppContextProvider>
|
||||
</ApiProvider>
|
||||
);
|
||||
@@ -447,6 +459,11 @@ export class AppManager implements BackstageApp {
|
||||
deps: {},
|
||||
factory: () => this.appIdentityProxy,
|
||||
});
|
||||
this.apiFactoryRegistry.register('static', {
|
||||
api: appTranslationApiRef,
|
||||
deps: {},
|
||||
factory: () => this.appTranslationApi,
|
||||
});
|
||||
|
||||
// It's possible to replace the feature flag API, but since we must have at least
|
||||
// one implementation we add it here directly instead of through the defaultApis.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2023 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, { PropsWithChildren } from 'react';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { appTranslationApiRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
|
||||
/** @alpha */
|
||||
export function AppTranslationProvider({ children }: PropsWithChildren<{}>) {
|
||||
const appTranslationAPi = useApi(appTranslationApiRef);
|
||||
const i18n = appTranslationAPi.getI18n();
|
||||
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2023 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 { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export type TranslationMessages<T> = T extends TranslationRef<infer R>
|
||||
? Record<string, Partial<R>>
|
||||
: never;
|
||||
|
||||
/** @alpha */
|
||||
export function createTranslationResource<T extends TranslationRef>(options: {
|
||||
ref: T;
|
||||
messages?: TranslationMessages<T>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{ messages: TranslationMessages<T> }>
|
||||
>;
|
||||
}) {
|
||||
return options;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
FeatureFlag,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/**
|
||||
* Props for the `BootErrorPage` component of {@link AppComponents}.
|
||||
@@ -177,6 +178,16 @@ export type AppRouteBinder = <
|
||||
>,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* TODO: To be remove when TranslationMessages in packages/core-app-api/src/app/TranslationResource.ts
|
||||
* come to be public
|
||||
*
|
||||
* @ignore
|
||||
* */
|
||||
type TranslationMessages<T> = T extends TranslationRef<infer R>
|
||||
? Record<string, Partial<R>>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* The options accepted by {@link createSpecializedApp}.
|
||||
*
|
||||
@@ -278,6 +289,23 @@ export type AppOptions = {
|
||||
* ```
|
||||
*/
|
||||
bindRoutes?(context: { bind: AppRouteBinder }): void;
|
||||
|
||||
/**
|
||||
* TODO: Change to ExperimentalI18n type when packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts
|
||||
* become to public
|
||||
*/
|
||||
__experimentalI18n?: {
|
||||
supportedLanguages: string[];
|
||||
fallbackLanguage?: string | string[];
|
||||
messages?: Array<{
|
||||
ref: TranslationRef;
|
||||
messages?: TranslationMessages<TranslationRef>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{ messages: TranslationMessages<TranslationRef> }>
|
||||
>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,9 +3,29 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { i18n } from 'i18next';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type AppTranslationApi = {
|
||||
getI18n(): i18n;
|
||||
addResourcesByRef<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
): void;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const appTranslationApiRef: ApiRef<AppTranslationApi>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const createTranslationRef: <
|
||||
Messages extends Record<keyof Messages, string> = {},
|
||||
>(
|
||||
config: TranslationRefConfig<Messages>,
|
||||
) => TranslationRef<Messages>;
|
||||
|
||||
// @alpha
|
||||
export interface PluginOptionsProviderProps {
|
||||
// (undocumented)
|
||||
@@ -17,10 +37,92 @@ export interface PluginOptionsProviderProps {
|
||||
// @alpha
|
||||
export const PluginProvider: (props: PluginOptionsProviderProps) => JSX.Element;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type TranslationOptions<
|
||||
Messages extends Record<keyof Messages, string> = Record<string, string>,
|
||||
> = Messages;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface TranslationRef<
|
||||
Messages extends Record<keyof Messages, string> = Record<string, string>,
|
||||
> {
|
||||
// (undocumented)
|
||||
getDefaultMessages(): Messages;
|
||||
// (undocumented)
|
||||
getId(): string;
|
||||
// (undocumented)
|
||||
getLazyResources():
|
||||
| Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: Messages;
|
||||
}>
|
||||
>
|
||||
| undefined;
|
||||
// (undocumented)
|
||||
getResources(): Record<string, Messages> | undefined;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface TranslationRefConfig<
|
||||
Messages extends Record<keyof Messages, string>,
|
||||
> {
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
lazyResources?: Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: Messages;
|
||||
}>
|
||||
>;
|
||||
// (undocumented)
|
||||
messages: Messages;
|
||||
// (undocumented)
|
||||
resources?: Record<string, Messages>;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export class TranslationRefImpl<Messages extends Record<keyof Messages, string>>
|
||||
implements TranslationRef<Messages>
|
||||
{
|
||||
// (undocumented)
|
||||
static create<Messages extends Record<keyof Messages, string>>(
|
||||
config: TranslationRefConfig<Messages>,
|
||||
): TranslationRefImpl<Messages>;
|
||||
// (undocumented)
|
||||
getDefaultMessages(): Messages;
|
||||
// (undocumented)
|
||||
getId(): string;
|
||||
// (undocumented)
|
||||
getLazyResources():
|
||||
| Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: Messages;
|
||||
}>
|
||||
>
|
||||
| undefined;
|
||||
// (undocumented)
|
||||
getResources(): Record<string, Messages> | undefined;
|
||||
// (undocumented)
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export function usePluginOptions<
|
||||
TPluginOptions extends {} = {},
|
||||
>(): TPluginOptions;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const useTranslationRef: <
|
||||
Messages extends Record<keyof Messages, string>,
|
||||
>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
) => <Tkey extends keyof Messages>(
|
||||
key: Tkey,
|
||||
options?: TranslationOptions,
|
||||
) => Messages[Tkey];
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -50,7 +50,9 @@
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"history": "^5.0.0",
|
||||
"i18next": "^22.4.15",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-i18next": "^12.3.1",
|
||||
"zen-observable": "^0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -15,3 +15,5 @@
|
||||
*/
|
||||
|
||||
export * from './plugin-options';
|
||||
export * from './translation';
|
||||
export * from './apis/alpha';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 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 * from './definitions/alpha';
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2023 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 { type i18n } from 'i18next';
|
||||
import { TranslationRef } from '../../translation';
|
||||
import { ApiRef, createApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
/** @alpha */
|
||||
export type AppTranslationApi = {
|
||||
getI18n(): i18n;
|
||||
|
||||
addResourcesByRef<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const appTranslationApiRef: ApiRef<AppTranslationApi> = createApiRef({
|
||||
id: 'core.apptranslation',
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 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 * from './AppTranslationApi';
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2023 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 { TranslationRefImpl, createTranslationRef } from './TranslationRef';
|
||||
|
||||
describe('TranslationRefImpl', () => {
|
||||
it('should create a TranslationRef instance', () => {
|
||||
const config = {
|
||||
id: 'testId',
|
||||
messages: { key: 'value' },
|
||||
};
|
||||
|
||||
const translationRef = TranslationRefImpl.create(config);
|
||||
|
||||
expect(translationRef.getId()).toBe('testId');
|
||||
expect(translationRef.getDefaultMessages()).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
it('should create a TranslationRef instance using the factory function', () => {
|
||||
const config = {
|
||||
id: 'testId',
|
||||
messages: { key: 'value' },
|
||||
};
|
||||
|
||||
const translationRef = createTranslationRef(config);
|
||||
|
||||
expect(translationRef.getId()).toBe('testId');
|
||||
expect(translationRef.getDefaultMessages()).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
it('should get lazy resources', async () => {
|
||||
const config = {
|
||||
id: 'testId',
|
||||
messages: { key: 'value' },
|
||||
lazyResources: {
|
||||
en: () => Promise.resolve({ messages: { key: 'value' } }),
|
||||
},
|
||||
};
|
||||
|
||||
const translationRef = TranslationRefImpl.create(config);
|
||||
|
||||
const lazyResources = translationRef.getLazyResources();
|
||||
|
||||
const messages = await lazyResources?.en();
|
||||
|
||||
expect(messages).toEqual({ messages: { key: 'value' } });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2023 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 { TranslationRef, TranslationRefConfig } from './types';
|
||||
|
||||
/** @alpha */
|
||||
export class TranslationRefImpl<Messages extends Record<keyof Messages, string>>
|
||||
implements TranslationRef<Messages>
|
||||
{
|
||||
static create<Messages extends Record<keyof Messages, string>>(
|
||||
config: TranslationRefConfig<Messages>,
|
||||
) {
|
||||
return new TranslationRefImpl(config);
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
getDefaultMessages(): Messages {
|
||||
return this.config.messages;
|
||||
}
|
||||
|
||||
getLazyResources():
|
||||
| Record<string, () => Promise<{ messages: Messages }>>
|
||||
| undefined {
|
||||
return this.config.lazyResources;
|
||||
}
|
||||
|
||||
getResources(): Record<string, Messages> | undefined {
|
||||
return this.config.resources;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `TranslationRef(${this.getId()})`;
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly config: TranslationRefConfig<Messages>,
|
||||
) {}
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export const createTranslationRef = <
|
||||
Messages extends Record<keyof Messages, string> = {},
|
||||
>(
|
||||
config: TranslationRefConfig<Messages>,
|
||||
): TranslationRef<Messages> => TranslationRefImpl.create(config);
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2023 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 * from './TranslationRef';
|
||||
export * from './types';
|
||||
export * from './useTranslationRef';
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2023 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.
|
||||
*/
|
||||
|
||||
/** @alpha */
|
||||
export interface TranslationRefConfig<
|
||||
Messages extends Record<keyof Messages, string>,
|
||||
> {
|
||||
id: string;
|
||||
messages: Messages;
|
||||
lazyResources?: Record<string, () => Promise<{ messages: Messages }>>;
|
||||
resources?: Record<string, Messages>;
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export interface TranslationRef<
|
||||
Messages extends Record<keyof Messages, string> = Record<string, string>,
|
||||
> {
|
||||
getId(): string;
|
||||
|
||||
getDefaultMessages(): Messages;
|
||||
|
||||
getResources(): Record<string, Messages> | undefined;
|
||||
|
||||
getLazyResources():
|
||||
| Record<string, () => Promise<{ messages: Messages }>>
|
||||
| undefined;
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export type TranslationOptions<
|
||||
Messages extends Record<keyof Messages, string> = Record<string, string>,
|
||||
> = Messages;
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2023 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useApi } from '../apis';
|
||||
import { useTranslationRef } from './useTranslationRef';
|
||||
import { createTranslationRef } from './TranslationRef';
|
||||
|
||||
jest.mock('../apis', () => ({
|
||||
...jest.requireActual('../apis'),
|
||||
useApi: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useTranslationRef', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return correct t', () => {
|
||||
const translationRef = createTranslationRef({
|
||||
id: 'ref-id',
|
||||
messages: {
|
||||
key1: 'default1',
|
||||
key2: 'default2',
|
||||
},
|
||||
});
|
||||
|
||||
const tMock = jest.fn();
|
||||
tMock.mockReturnValue('translatedValue');
|
||||
|
||||
const i18nMock = {
|
||||
language: 'en',
|
||||
t: tMock,
|
||||
};
|
||||
|
||||
(useApi as jest.Mock).mockReturnValue({
|
||||
addResourcesByRef: jest.fn(),
|
||||
});
|
||||
|
||||
(useTranslation as jest.Mock).mockReturnValue(i18nMock);
|
||||
|
||||
const { result } = renderHook(() => useTranslationRef(translationRef));
|
||||
|
||||
const t = result.current;
|
||||
|
||||
t('key1', { condition: 'v1' });
|
||||
expect(tMock).toHaveBeenCalledWith('key1', 'default1', { condition: 'v1' });
|
||||
t('key2');
|
||||
expect(tMock).toHaveBeenCalledWith('key2', 'default2', undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2023 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 { useTranslation } from 'react-i18next';
|
||||
|
||||
import { TranslationOptions, TranslationRef } from './types';
|
||||
import { useApi } from '../apis';
|
||||
import { appTranslationApiRef } from '../apis/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export const useTranslationRef = <
|
||||
Messages extends Record<keyof Messages, string>,
|
||||
>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
) => {
|
||||
const appTranslationApi = useApi(appTranslationApiRef);
|
||||
|
||||
appTranslationApi.addResourcesByRef(translationRef);
|
||||
|
||||
const { t } = useTranslation(translationRef.getId());
|
||||
|
||||
const defaulteMessage = translationRef.getDefaultMessages();
|
||||
|
||||
return <Tkey extends keyof Messages>(
|
||||
key: Tkey,
|
||||
options?: TranslationOptions,
|
||||
): Messages[Tkey] => t(key as string, defaulteMessage[key], options);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
## API Report File for "@backstage/plugin-user-settings"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const userSettingsTranslationRef: TranslationRef<{
|
||||
language: string;
|
||||
change_the_language: string;
|
||||
theme: string;
|
||||
theme_light: string;
|
||||
theme_dark: string;
|
||||
theme_auto: string;
|
||||
change_the_theme_mode: string;
|
||||
select_theme_light: string;
|
||||
select_theme_dark: string;
|
||||
select_theme_auto: string;
|
||||
select_theme_custom: string;
|
||||
lng: string;
|
||||
select_lng: string;
|
||||
}>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -93,6 +93,9 @@ export const UserSettingsGeneral: () => React_2.JSX.Element;
|
||||
// @public (undocumented)
|
||||
export const UserSettingsIdentityCard: () => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const UserSettingsLanguageToggle: () => React_2.JSX.Element | null;
|
||||
|
||||
// @public (undocumented)
|
||||
export const UserSettingsMenu: () => React_2.JSX.Element;
|
||||
|
||||
|
||||
@@ -5,10 +5,23 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"alpha": [
|
||||
"src/alpha.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
"access": "public"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin"
|
||||
@@ -43,6 +56,7 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-use": "^17.2.4",
|
||||
"zen-observable": "^0.10.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 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 * from './translation';
|
||||
@@ -19,6 +19,7 @@ import { List } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { UserSettingsPinToggle } from './UserSettingsPinToggle';
|
||||
import { UserSettingsThemeToggle } from './UserSettingsThemeToggle';
|
||||
import { UserSettingsLanguageToggle } from './UserSettingsLanguageToggle';
|
||||
|
||||
/** @public */
|
||||
export const UserSettingsAppearanceCard = () => {
|
||||
@@ -28,6 +29,7 @@ export const UserSettingsAppearanceCard = () => {
|
||||
<InfoCard title="Appearance" variant="gridItem">
|
||||
<List dense>
|
||||
<UserSettingsThemeToggle />
|
||||
<UserSettingsLanguageToggle />
|
||||
{!isMobile && <UserSettingsPinToggle />}
|
||||
</List>
|
||||
</InfoCard>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2023 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 from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { UserSettingsLanguageToggle } from './UserSettingsLanguageToggle';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
jest.mock('@backstage/core-plugin-api/alpha', () => ({
|
||||
...jest.requireActual('@backstage/core-plugin-api/alpha'),
|
||||
useTranslationRef: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
...jest.requireActual('react-i18next'),
|
||||
useTranslation: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('UserSettingsLanguageToggle', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render correctly with multiple supported languages', () => {
|
||||
const messages: Record<string, string> = {
|
||||
en: 'English',
|
||||
fr: 'French',
|
||||
de: 'German',
|
||||
language: 'language',
|
||||
change_the_language: 'Change the language',
|
||||
};
|
||||
|
||||
const i18nMock = {
|
||||
language: 'en',
|
||||
options: {
|
||||
supportedLngs: ['en', 'fr', 'de'],
|
||||
},
|
||||
changeLanguage: jest.fn(),
|
||||
};
|
||||
|
||||
(useTranslation as jest.Mock).mockReturnValue({
|
||||
i18n: i18nMock,
|
||||
});
|
||||
|
||||
(useTranslationRef as jest.Mock).mockReturnValue(
|
||||
(key: string, option: any) =>
|
||||
messages[option?.language || key] || 'translatedValue',
|
||||
);
|
||||
|
||||
render(wrapInTestApp(<UserSettingsLanguageToggle />));
|
||||
|
||||
expect(screen.getAllByText('Change the language')).toHaveLength(1);
|
||||
expect(screen.getAllByText('English')).toHaveLength(1);
|
||||
expect(screen.getAllByText('French')).toHaveLength(1);
|
||||
expect(screen.getAllByText('German')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not render when only one supported language', () => {
|
||||
const tMock = jest.fn().mockReturnValue('translatedValue');
|
||||
const i18nMock = {
|
||||
language: 'en',
|
||||
options: {
|
||||
supportedLngs: ['en'],
|
||||
},
|
||||
changeLanguage: jest.fn(),
|
||||
};
|
||||
|
||||
(useTranslationRef as jest.Mock).mockReturnValue(tMock);
|
||||
|
||||
(useTranslation as jest.Mock).mockReturnValue({
|
||||
i18n: i18nMock,
|
||||
});
|
||||
|
||||
render(wrapInTestApp(<UserSettingsLanguageToggle />));
|
||||
|
||||
expect(screen.queryByText('translatedValue')).toBeNull();
|
||||
expect(screen.queryByText('English')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle language change', () => {
|
||||
const messages: Record<string, string> = {
|
||||
en: 'English',
|
||||
fr: 'French',
|
||||
language: 'language',
|
||||
change_the_language: 'Change the language',
|
||||
};
|
||||
|
||||
const i18nMock = {
|
||||
language: 'en',
|
||||
options: {
|
||||
supportedLngs: ['en', 'fr'],
|
||||
},
|
||||
changeLanguage: jest.fn(),
|
||||
};
|
||||
|
||||
(useTranslationRef as jest.Mock).mockReturnValue(
|
||||
(key: string, option: any) =>
|
||||
messages[option?.language || key] || 'translatedValue',
|
||||
);
|
||||
|
||||
(useTranslation as jest.Mock).mockReturnValue({
|
||||
i18n: i18nMock,
|
||||
});
|
||||
|
||||
render(wrapInTestApp(<UserSettingsLanguageToggle />));
|
||||
|
||||
fireEvent.click(screen.getByText('French'));
|
||||
|
||||
expect(i18nMock.changeLanguage).toHaveBeenCalledWith('fr');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import ToggleButton from '@material-ui/lab/ToggleButton';
|
||||
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
|
||||
import {
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
Tooltip,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import { userSettingsTranslationRef } from '../../translation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type TooltipToggleButtonProps = {
|
||||
children: JSX.Element;
|
||||
title: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 8,
|
||||
paddingRight: 16,
|
||||
},
|
||||
list: {
|
||||
width: 'initial',
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
width: '100%',
|
||||
padding: `0 0 12px`,
|
||||
},
|
||||
},
|
||||
listItemText: {
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0,
|
||||
},
|
||||
listItemSecondaryAction: {
|
||||
position: 'relative',
|
||||
transform: 'unset',
|
||||
top: 'auto',
|
||||
right: 'auto',
|
||||
paddingLeft: 16,
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// ToggleButtonGroup uses React.children.map instead of context
|
||||
// so wrapping with Tooltip breaks ToggleButton functionality.
|
||||
const TooltipToggleButton = ({
|
||||
children,
|
||||
title,
|
||||
value,
|
||||
...props
|
||||
}: TooltipToggleButtonProps) => (
|
||||
<Tooltip placement="top" arrow title={title}>
|
||||
<ToggleButton value={value} {...props}>
|
||||
{children}
|
||||
</ToggleButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const UserSettingsLanguageToggle = () => {
|
||||
const classes = useStyles();
|
||||
const { i18n } = useTranslation();
|
||||
const t = useTranslationRef(userSettingsTranslationRef);
|
||||
|
||||
const supportedLngs = useMemo(
|
||||
() => (i18n.options.supportedLngs || []).filter(lng => lng !== 'cimode'),
|
||||
[i18n],
|
||||
);
|
||||
|
||||
if (supportedLngs.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSetLanguage = (
|
||||
_event: React.MouseEvent<HTMLElement>,
|
||||
newLanguage: string | undefined,
|
||||
) => {
|
||||
if (supportedLngs.some(it => it === newLanguage)) {
|
||||
i18n.changeLanguage(newLanguage);
|
||||
} else {
|
||||
i18n.changeLanguage(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
className={classes.list}
|
||||
classes={{ container: classes.container }}
|
||||
>
|
||||
<ListItemText
|
||||
className={classes.listItemText}
|
||||
primary={t('language')}
|
||||
secondary={t('change_the_language')}
|
||||
/>
|
||||
<ListItemSecondaryAction className={classes.listItemSecondaryAction}>
|
||||
<ToggleButtonGroup
|
||||
exclusive
|
||||
size="small"
|
||||
value={i18n.language}
|
||||
onChange={handleSetLanguage}
|
||||
>
|
||||
{supportedLngs.map(lng => {
|
||||
return (
|
||||
<TooltipToggleButton
|
||||
key={lng}
|
||||
title={t('select_lng', {
|
||||
language: lng,
|
||||
})}
|
||||
value={lng}
|
||||
>
|
||||
<>
|
||||
{t('lng', {
|
||||
language: lng,
|
||||
})}
|
||||
</>
|
||||
</TooltipToggleButton>
|
||||
);
|
||||
})}
|
||||
</ToggleButtonGroup>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { AppTheme, appThemeApiRef } from '@backstage/core-plugin-api';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import {
|
||||
renderWithEffects,
|
||||
TestApiRegistry,
|
||||
@@ -27,6 +28,7 @@ import { fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { UserSettingsThemeToggle } from './UserSettingsThemeToggle';
|
||||
import { ApiProvider, AppThemeSelector } from '@backstage/core-app-api';
|
||||
import { userSettingsTranslationRef } from '../../translation';
|
||||
|
||||
const mockTheme: AppTheme = {
|
||||
id: 'light-theme',
|
||||
@@ -39,6 +41,11 @@ const mockTheme: AppTheme = {
|
||||
),
|
||||
};
|
||||
|
||||
jest.mock('@backstage/core-plugin-api/alpha', () => ({
|
||||
...jest.requireActual('@backstage/core-plugin-api/alpha'),
|
||||
useTranslationRef: jest.fn(),
|
||||
}));
|
||||
|
||||
const apiRegistry = TestApiRegistry.from([
|
||||
appThemeApiRef,
|
||||
AppThemeSelector.createWithStorage([mockTheme]),
|
||||
@@ -47,6 +54,16 @@ const apiRegistry = TestApiRegistry.from([
|
||||
describe('<UserSettingsThemeToggle />', () => {
|
||||
it('toggles the theme select button', async () => {
|
||||
const themeApi = apiRegistry.get(appThemeApiRef);
|
||||
// todo: general test provider
|
||||
const messages: Record<string, string> =
|
||||
userSettingsTranslationRef.getDefaultMessages();
|
||||
|
||||
const useTranslationRefMock = jest
|
||||
.fn()
|
||||
.mockReturnValue((key: string) => messages[key]);
|
||||
|
||||
(useTranslationRef as jest.Mock).mockImplementation(useTranslationRefMock);
|
||||
|
||||
const rendered = await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
@@ -57,7 +74,7 @@ describe('<UserSettingsThemeToggle />', () => {
|
||||
|
||||
expect(rendered.getByText('Theme')).toBeInTheDocument();
|
||||
|
||||
const themeButton = rendered.getByTitle('Select Mock Theme');
|
||||
const themeButton = rendered.getByText('Mock Theme');
|
||||
expect(themeApi?.getActiveThemeId()).toBe(undefined);
|
||||
fireEvent.click(themeButton);
|
||||
expect(themeApi?.getActiveThemeId()).toBe('light-theme');
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import { appThemeApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { userSettingsTranslationRef } from '../../translation';
|
||||
|
||||
type ThemeIconProps = {
|
||||
id: string;
|
||||
@@ -101,18 +103,20 @@ const TooltipToggleButton = ({
|
||||
export const UserSettingsThemeToggle = () => {
|
||||
const classes = useStyles();
|
||||
const appThemeApi = useApi(appThemeApiRef);
|
||||
const themeId = useObservable(
|
||||
const activeThemeId = useObservable(
|
||||
appThemeApi.activeThemeId$(),
|
||||
appThemeApi.getActiveThemeId(),
|
||||
);
|
||||
|
||||
const themeIds = appThemeApi.getInstalledThemes();
|
||||
|
||||
const t = useTranslationRef(userSettingsTranslationRef);
|
||||
|
||||
const handleSetTheme = (
|
||||
_event: React.MouseEvent<HTMLElement>,
|
||||
newThemeId: string | undefined,
|
||||
) => {
|
||||
if (themeIds.some(t => t.id === newThemeId)) {
|
||||
if (themeIds.some(it => it.id === newThemeId)) {
|
||||
appThemeApi.setActiveThemeId(newThemeId);
|
||||
} else {
|
||||
appThemeApi.setActiveThemeId(undefined);
|
||||
@@ -126,26 +130,31 @@ export const UserSettingsThemeToggle = () => {
|
||||
>
|
||||
<ListItemText
|
||||
className={classes.listItemText}
|
||||
primary="Theme"
|
||||
secondary="Change the theme mode"
|
||||
primary={t('theme')}
|
||||
secondary={t('change_the_theme_mode')}
|
||||
/>
|
||||
<ListItemSecondaryAction className={classes.listItemSecondaryAction}>
|
||||
<ToggleButtonGroup
|
||||
exclusive
|
||||
size="small"
|
||||
value={themeId ?? 'auto'}
|
||||
value={activeThemeId ?? 'auto'}
|
||||
onChange={handleSetTheme}
|
||||
>
|
||||
{themeIds.map(theme => {
|
||||
const themeIcon = themeIds.find(t => t.id === theme.id)?.icon;
|
||||
const themeIcon = themeIds.find(it => it.id === theme.id)?.icon;
|
||||
const themeId = theme.id as 'light' | 'dark';
|
||||
return (
|
||||
<TooltipToggleButton
|
||||
key={theme.id}
|
||||
title={`Select ${theme.title}`}
|
||||
title={
|
||||
theme.title
|
||||
? t('select_theme_custom', { custom: theme.title })
|
||||
: t(`select_theme_${themeId}`)
|
||||
}
|
||||
value={theme.id}
|
||||
>
|
||||
<>
|
||||
{theme.title}
|
||||
{theme.title || t(`theme_${themeId}`)}
|
||||
<ThemeIcon
|
||||
id={theme.id}
|
||||
icon={themeIcon}
|
||||
@@ -155,10 +164,12 @@ export const UserSettingsThemeToggle = () => {
|
||||
</TooltipToggleButton>
|
||||
);
|
||||
})}
|
||||
<Tooltip placement="top" arrow title="Select Auto Theme">
|
||||
<ToggleButton value="auto" selected={themeId === undefined}>
|
||||
Auto
|
||||
<AutoIcon color={themeId === undefined ? 'primary' : undefined} />
|
||||
<Tooltip placement="top" arrow title={t('select_theme_auto')}>
|
||||
<ToggleButton value="auto" selected={activeThemeId === undefined}>
|
||||
{t('theme_auto')}
|
||||
<AutoIcon
|
||||
color={activeThemeId === undefined ? 'primary' : undefined}
|
||||
/>
|
||||
</ToggleButton>
|
||||
</Tooltip>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
@@ -22,3 +22,4 @@ export { UserSettingsAppearanceCard } from './UserSettingsAppearanceCard';
|
||||
export { UserSettingsThemeToggle } from './UserSettingsThemeToggle';
|
||||
export { UserSettingsPinToggle } from './UserSettingsPinToggle';
|
||||
export { UserSettingsIdentityCard } from './UserSettingsIdentityCard';
|
||||
export { UserSettingsLanguageToggle } from './UserSettingsLanguageToggle';
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2023 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 { createTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export const userSettingsTranslationRef = createTranslationRef({
|
||||
id: 'user-settings',
|
||||
messages: {
|
||||
language: 'Language',
|
||||
change_the_language: 'Change the language',
|
||||
theme: 'Theme',
|
||||
theme_light: 'Light',
|
||||
theme_dark: 'Dark',
|
||||
theme_auto: 'Auto',
|
||||
change_the_theme_mode: 'Change the theme mode',
|
||||
select_theme_light: 'Select light',
|
||||
select_theme_dark: 'Select dark',
|
||||
select_theme_auto: 'Select Auto Theme',
|
||||
select_theme_custom: 'Select {{custom}}',
|
||||
lng: '{{language}}',
|
||||
select_lng: 'Select language {{language}}',
|
||||
},
|
||||
});
|
||||
@@ -3305,7 +3305,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.10, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
|
||||
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.10, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
|
||||
version: 7.22.11
|
||||
resolution: "@babel/runtime@npm:7.22.11"
|
||||
dependencies:
|
||||
@@ -3987,8 +3987,11 @@ __metadata:
|
||||
"@types/react": ^16.13.1 || ^17.0.0
|
||||
"@types/zen-observable": ^0.8.0
|
||||
history: ^5.0.0
|
||||
i18next: ^22.4.15
|
||||
i18next-browser-languagedetector: ^7.0.2
|
||||
msw: ^1.0.0
|
||||
prop-types: ^15.7.2
|
||||
react-i18next: ^12.3.1
|
||||
react-router-beta: "npm:react-router@6.0.0-beta.0"
|
||||
react-router-dom-beta: "npm:react-router-dom@6.0.0-beta.0"
|
||||
react-router-dom-stable: "npm:react-router-dom@^6.3.0"
|
||||
@@ -4217,8 +4220,10 @@ __metadata:
|
||||
"@types/react": ^16.13.1 || ^17.0.0
|
||||
"@types/zen-observable": ^0.8.0
|
||||
history: ^5.0.0
|
||||
i18next: ^22.4.15
|
||||
msw: ^1.0.0
|
||||
prop-types: ^15.7.2
|
||||
react-i18next: ^12.3.1
|
||||
zen-observable: ^0.10.0
|
||||
peerDependencies:
|
||||
react: ^16.13.1 || ^17.0.0
|
||||
@@ -9801,6 +9806,7 @@ __metadata:
|
||||
"@types/react": ^16.13.1 || ^17.0.0
|
||||
cross-fetch: ^3.1.5
|
||||
msw: ^1.0.0
|
||||
react-i18next: ^12.3.1
|
||||
react-use: ^17.2.4
|
||||
zen-observable: ^0.10.0
|
||||
peerDependencies:
|
||||
@@ -28084,6 +28090,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-parse-stringify@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "html-parse-stringify@npm:3.0.1"
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
checksum: 334fdebd4b5c355dba8e95284cead6f62bf865a2359da2759b039db58c805646350016d2017875718bc3c4b9bf81a0d11be5ee0cf4774a3a5a7b97cde21cfd67
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-webpack-plugin@npm:^5.3.1":
|
||||
version: 5.5.3
|
||||
resolution: "html-webpack-plugin@npm:5.5.3"
|
||||
@@ -28329,6 +28344,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"i18next-browser-languagedetector@npm:^7.0.2":
|
||||
version: 7.1.0
|
||||
resolution: "i18next-browser-languagedetector@npm:7.1.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.19.4
|
||||
checksum: 36981b9a9995ed66387f3735cceffe107ed3cdb6ca278d45fa243fabc65669c0eca095ed4a55a93dac046ca1eb23fd986ec0079723be7ebb8505e6ba25f379bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"i18next@npm:^22.4.15":
|
||||
version: 22.5.1
|
||||
resolution: "i18next@npm:22.5.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.20.6
|
||||
checksum: 175f8ab7fac2abcee147b00cc2d8e7d4fa9b05cdc227f02cac841fc2fd9545ed4a6d88774f594f8ad12dc944e4d34cc8e88aa00c8b9947baef9e859d93abd305
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24":
|
||||
version: 0.4.24
|
||||
resolution: "iconv-lite@npm:0.4.24"
|
||||
@@ -37098,6 +37131,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-i18next@npm:^12.3.1":
|
||||
version: 12.3.1
|
||||
resolution: "react-i18next@npm:12.3.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.20.6
|
||||
html-parse-stringify: ^3.0.1
|
||||
peerDependencies:
|
||||
i18next: ">= 19.0.0"
|
||||
react: ">= 16.8.0"
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
checksum: fe3f360e5184bc63861734e94bf625a09b9ec0d28fab41779a68758af258fd1737dde25ff7a88ddb66c1571a3e3de5b3403825a91b4949bf9832a00615acb87a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-immutable-proptypes@npm:2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "react-immutable-proptypes@npm:2.2.0"
|
||||
@@ -42434,6 +42485,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"void-elements@npm:3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "void-elements@npm:3.1.0"
|
||||
checksum: 0390f818107fa8fce55bb0a5c3f661056001c1d5a2a48c28d582d4d847347c2ab5b7f8272314cac58acf62345126b6b09bea623a185935f6b1c3bbce0dfd7f7f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vscode-languageserver-types@npm:^3.15.1":
|
||||
version: 3.15.1
|
||||
resolution: "vscode-languageserver-types@npm:3.15.1"
|
||||
|
||||
Reference in New Issue
Block a user