feat: Introduced experimental support for internationalization.
Signed-off-by: rui ma <ruima@alauda.io>
This commit is contained in:
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user