Merge pull request #19898 from backstage/rugvip/i18n-resources
i18n: refactor TranslationRef and rework TranslationMessages and TranslationResource
This commit is contained in:
@@ -1,85 +0,0 @@
|
||||
## 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?: Record<
|
||||
string,
|
||||
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?: Record<string, TranslationMessages<T>>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: TranslationMessages<T>;
|
||||
}>
|
||||
>;
|
||||
}): {
|
||||
ref: T;
|
||||
messages?: Record<string, 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?: Record<string, TranslationMessages<TranslationRef>>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: TranslationMessages<TranslationRef>;
|
||||
}>
|
||||
>;
|
||||
}>;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type TranslationMessages<T> = T extends TranslationRef<infer R>
|
||||
? Partial<R>
|
||||
: never;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -64,7 +64,8 @@ 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';
|
||||
import { TranslationMessages } from '@backstage/core-plugin-api/alpha';
|
||||
import { TranslationResource } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @public
|
||||
export class AlertApiForwarder implements AlertApi {
|
||||
@@ -221,19 +222,10 @@ export type AppOptions = {
|
||||
themes: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
|
||||
configLoader?: AppConfigLoader;
|
||||
bindRoutes?(context: { bind: AppRouteBinder }): void;
|
||||
__experimentalI18n?: {
|
||||
supportedLanguages: string[];
|
||||
__experimentalTranslations?: {
|
||||
fallbackLanguage?: string | string[];
|
||||
messages?: Array<{
|
||||
ref: TranslationRef;
|
||||
messages?: Record<string, TranslationMessages<TranslationRef>>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: TranslationMessages<TranslationRef>;
|
||||
}>
|
||||
>;
|
||||
}>;
|
||||
supportedLanguages?: string[];
|
||||
resources?: Array<TranslationMessages | TranslationResource>;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,14 +7,10 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"alpha": [
|
||||
"src/alpha.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
|
||||
+39
-79
@@ -14,7 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import {
|
||||
createTranslationMessages,
|
||||
createTranslationRef,
|
||||
createTranslationResource,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
import { AppTranslationApiImpl } from './AppTranslationImpl';
|
||||
import i18next from 'i18next';
|
||||
|
||||
@@ -70,112 +74,68 @@ describe('AppTranslationApiImpl', () => {
|
||||
});
|
||||
|
||||
it('should init messages correctly', () => {
|
||||
const useResourcesMock = jest.spyOn(
|
||||
const addMessagesMock = jest.spyOn(
|
||||
AppTranslationApiImpl.prototype,
|
||||
'addResources',
|
||||
'addMessages',
|
||||
);
|
||||
const useLazyResourcesMock = jest.spyOn(
|
||||
const addLazyResourcesMock = jest.spyOn(
|
||||
AppTranslationApiImpl.prototype,
|
||||
'addLazyResources',
|
||||
);
|
||||
const ref = createTranslationRef({
|
||||
id: 'ref-id',
|
||||
messages: {
|
||||
key1: '',
|
||||
key: '',
|
||||
},
|
||||
});
|
||||
|
||||
const options = {
|
||||
supportedLanguages: ['en'],
|
||||
messages: [
|
||||
{
|
||||
ref,
|
||||
messages: {
|
||||
en: { key1: 'value1' },
|
||||
},
|
||||
lazyMessages: {
|
||||
en: () => Promise.resolve({ key2: 'value2' }),
|
||||
} as any,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const instance = AppTranslationApiImpl.create({
|
||||
supportedLanguages: ['en'],
|
||||
const overrides = createTranslationMessages({
|
||||
ref,
|
||||
messages: { key: 'value1' },
|
||||
});
|
||||
const resource = createTranslationResource({
|
||||
ref,
|
||||
translations: {
|
||||
en: () =>
|
||||
Promise.resolve({
|
||||
default: {
|
||||
key: 'value2',
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
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,
|
||||
);
|
||||
AppTranslationApiImpl.create({
|
||||
supportedLanguages: ['en'],
|
||||
resources: [overrides, resource],
|
||||
});
|
||||
|
||||
expect(addMessagesMock).toHaveBeenCalledWith(overrides);
|
||||
expect(addLazyResourcesMock).toHaveBeenCalledWith(resource);
|
||||
});
|
||||
|
||||
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,
|
||||
const addLazyResourcesMock = jest.spyOn(
|
||||
AppTranslationApiImpl.prototype,
|
||||
'addLazyResources',
|
||||
);
|
||||
});
|
||||
|
||||
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' } }),
|
||||
translations: {
|
||||
en: () => Promise.resolve({ default: { key1: 'value2' } }),
|
||||
},
|
||||
});
|
||||
|
||||
const instance = AppTranslationApiImpl.create({
|
||||
supportedLanguages: ['en'],
|
||||
});
|
||||
instance.addLazyResources(ref);
|
||||
setTimeout(() => {
|
||||
expect(useReturnMock.addResourceBundle).toHaveBeenCalledWith(
|
||||
'en',
|
||||
'ref-id',
|
||||
{ key1: 'value2' },
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
instance.addResource(ref);
|
||||
|
||||
expect(addLazyResourcesMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLazyResourcesMock.mock.calls[0][0].id).toBe('ref-id');
|
||||
});
|
||||
});
|
||||
+56
-60
@@ -16,28 +16,39 @@
|
||||
|
||||
import {
|
||||
AppTranslationApi,
|
||||
TranslationMessages,
|
||||
TranslationRef,
|
||||
TranslationResource,
|
||||
} 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';
|
||||
// Internal import to avoid code duplication, this will lead to duplication in build output
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalTranslationResource } from '../../../../../core-plugin-api/src/translation/TranslationResource';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalTranslationRef } from '../../../../../core-plugin-api/src/translation/TranslationRef';
|
||||
|
||||
const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
/** @alpha */
|
||||
export type ExperimentalI18n = {
|
||||
supportedLanguages: string[];
|
||||
fallbackLanguage?: string | string[];
|
||||
messages?: Array<{
|
||||
ref: TranslationRef;
|
||||
messages?: Record<string, TranslationMessages<TranslationRef>>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{ messages: TranslationMessages<TranslationRef> }>
|
||||
>;
|
||||
}>;
|
||||
supportedLanguages?: string[];
|
||||
resources?: Array<TranslationMessages | TranslationResource>;
|
||||
};
|
||||
|
||||
function removeNulls(
|
||||
messages: Record<string, string | null>,
|
||||
): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(messages).filter(
|
||||
(e): e is [string, string] => e[1] !== null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export class AppTranslationApiImpl implements AppTranslationApi {
|
||||
static create(options?: ExperimentalI18n) {
|
||||
@@ -46,8 +57,8 @@ export class AppTranslationApiImpl implements AppTranslationApi {
|
||||
i18n.use(LanguageDetector);
|
||||
|
||||
i18n.init({
|
||||
fallbackLng: options?.fallbackLanguage || 'en',
|
||||
supportedLngs: options?.supportedLanguages || ['en'],
|
||||
fallbackLng: options?.fallbackLanguage || DEFAULT_LANGUAGE,
|
||||
supportedLngs: options?.supportedLanguages || [DEFAULT_LANGUAGE],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
@@ -59,69 +70,52 @@ export class AppTranslationApiImpl implements AppTranslationApi {
|
||||
return new AppTranslationApiImpl(i18n, options);
|
||||
}
|
||||
|
||||
private readonly cache = new WeakSet<TranslationRef>();
|
||||
private readonly lazyCache = new WeakMap<TranslationRef, Set<string>>();
|
||||
private readonly cache = new Set<string>();
|
||||
private readonly lazyCache = new Map<string, 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);
|
||||
}
|
||||
});
|
||||
for (const resource of options?.resources || []) {
|
||||
if (resource.$$type === '@backstage/TranslationResource') {
|
||||
this.addLazyResources(resource);
|
||||
} else if (resource.$$type === '@backstage/TranslationMessages') {
|
||||
// Overrides for default messages, created with createTranslationMessages and installed via app
|
||||
this.addMessages(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addResourcesByRef<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
): void {
|
||||
this.addResources(translationRef);
|
||||
this.addLazyResources(translationRef);
|
||||
addResource(translationRef: TranslationRef): void {
|
||||
const internalRef = toInternalTranslationRef(translationRef);
|
||||
const defaultResource = internalRef.getDefaultResource();
|
||||
if (defaultResource) {
|
||||
this.addLazyResources(defaultResource);
|
||||
}
|
||||
}
|
||||
|
||||
addResources<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
initResources?: Record<
|
||||
string,
|
||||
TranslationMessages<TranslationRef<Messages>>
|
||||
>,
|
||||
) {
|
||||
const resources = initResources || translationRef.getResources();
|
||||
if (!resources || this.cache.has(translationRef)) {
|
||||
addMessages(messages: TranslationMessages) {
|
||||
if (this.cache.has(messages.id)) {
|
||||
return;
|
||||
}
|
||||
this.cache.add(translationRef);
|
||||
Object.entries(resources).forEach(([language, messages]) => {
|
||||
this.i18n.addResourceBundle(
|
||||
language,
|
||||
translationRef.getId(),
|
||||
messages,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
this.cache.add(messages.id);
|
||||
this.i18n.addResourceBundle(
|
||||
DEFAULT_LANGUAGE,
|
||||
messages.id,
|
||||
removeNulls(messages.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);
|
||||
addLazyResources(resource: TranslationResource) {
|
||||
let cache = this.lazyCache.get(resource.id);
|
||||
|
||||
if (!cache) {
|
||||
cache = new Set();
|
||||
this.lazyCache.set(translationRef, cache);
|
||||
this.lazyCache.set(resource.id, cache);
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -136,8 +130,8 @@ export class AppTranslationApiImpl implements AppTranslationApi {
|
||||
return;
|
||||
}
|
||||
|
||||
const namespace = translationRef.getId();
|
||||
const lazyResources = initResources || translationRef.getLazyResources();
|
||||
const internalResource = toInternalTranslationResource(resource);
|
||||
const namespace = internalResource.id;
|
||||
|
||||
Promise.allSettled((options.supportedLngs || []).map(addLanguage)).then(
|
||||
results => {
|
||||
@@ -160,7 +154,9 @@ export class AppTranslationApiImpl implements AppTranslationApi {
|
||||
loadBackend = reloadResources([language], [namespace]);
|
||||
}
|
||||
|
||||
const loadLazyResources = lazyResources?.[language];
|
||||
const loadLazyResources = internalResource.resources.find(
|
||||
entry => entry.language === language,
|
||||
)?.loader;
|
||||
|
||||
if (!loadLazyResources) {
|
||||
await loadBackend;
|
||||
|
||||
@@ -260,7 +260,7 @@ describe('Integration Test', () => {
|
||||
expect(screen.getByText('extLink4: <none>')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('runs success with __experimentalI18n', async () => {
|
||||
it('runs success with __experimentalTranslations', async () => {
|
||||
const app = new AppManager({
|
||||
apis: [noOpAnalyticsApi],
|
||||
defaultApis: [],
|
||||
@@ -275,7 +275,7 @@ describe('Integration Test', () => {
|
||||
extRouteRef2: plugin2RouteRef,
|
||||
});
|
||||
},
|
||||
__experimentalI18n: {
|
||||
__experimentalTranslations: {
|
||||
supportedLanguages: ['en'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -179,7 +179,7 @@ export class AppManager implements BackstageApp {
|
||||
this.bindRoutes = options.bindRoutes;
|
||||
this.apiFactoryRegistry = new ApiFactoryRegistry();
|
||||
this.appTranslationApi = AppTranslationApiImpl.create(
|
||||
options.__experimentalI18n,
|
||||
options.__experimentalTranslations,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ import {
|
||||
FeatureFlag,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import {
|
||||
TranslationMessages,
|
||||
TranslationResource,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/**
|
||||
* Props for the `BootErrorPage` component of {@link AppComponents}.
|
||||
@@ -178,16 +181,6 @@ 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>
|
||||
? Partial<R>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* The options accepted by {@link createSpecializedApp}.
|
||||
*
|
||||
@@ -290,21 +283,11 @@ 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[];
|
||||
// TODO: Change to ExperimentalI18n type when packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts
|
||||
__experimentalTranslations?: {
|
||||
fallbackLanguage?: string | string[];
|
||||
messages?: Array<{
|
||||
ref: TranslationRef;
|
||||
messages?: Record<string, TranslationMessages<TranslationRef>>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{ messages: TranslationMessages<TranslationRef> }>
|
||||
>;
|
||||
}>;
|
||||
supportedLanguages?: string[];
|
||||
resources?: Array<TranslationMessages | TranslationResource>;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,24 +7,64 @@ import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { i18n } from 'i18next';
|
||||
import { ReactNode } from 'react';
|
||||
import { TranslationMessages as TranslationMessages_2 } from '@backstage/core-plugin-api/alpha';
|
||||
import { TranslationRef as TranslationRef_2 } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type AppTranslationApi = {
|
||||
getI18n(): i18n;
|
||||
addResourcesByRef<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
): void;
|
||||
addResource(resource: TranslationRef): void;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const appTranslationApiRef: ApiRef<AppTranslationApi>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const createTranslationRef: <
|
||||
Messages extends Record<keyof Messages, string> = {},
|
||||
// @alpha
|
||||
export function createTranslationMessages<
|
||||
TId extends string,
|
||||
TMessages extends {
|
||||
[key in string]: string;
|
||||
},
|
||||
TFull extends boolean,
|
||||
>(
|
||||
config: TranslationRefConfig<Messages>,
|
||||
) => TranslationRef<Messages>;
|
||||
options: TranslationMessagesOptions<TId, TMessages, TFull>,
|
||||
): TranslationMessages<TId, TMessages, TFull>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export function createTranslationRef<
|
||||
TId extends string,
|
||||
TMessages extends {
|
||||
[key in string]: string;
|
||||
},
|
||||
TTranslations extends {
|
||||
[language in string]: () => Promise<{
|
||||
default: {
|
||||
[key in keyof TMessages]: string | null;
|
||||
};
|
||||
}>;
|
||||
},
|
||||
>(
|
||||
config: TranslationRefOptions<TId, TMessages, TTranslations>,
|
||||
): TranslationRef<TId, TMessages>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export function createTranslationResource<
|
||||
TId extends string,
|
||||
TMessages extends {
|
||||
[key in string]: string;
|
||||
},
|
||||
TTranslations extends {
|
||||
[language in string]: () => Promise<{
|
||||
default:
|
||||
| TranslationMessages_2<TId>
|
||||
| {
|
||||
[key in keyof TMessages]: string | null;
|
||||
};
|
||||
}>;
|
||||
},
|
||||
>(
|
||||
options: TranslationResourceOptions<TId, TMessages, TTranslations>,
|
||||
): TranslationResource<TId>;
|
||||
|
||||
// @alpha
|
||||
export interface PluginOptionsProviderProps {
|
||||
@@ -37,76 +77,115 @@ export interface PluginOptionsProviderProps {
|
||||
// @alpha
|
||||
export const PluginProvider: (props: PluginOptionsProviderProps) => JSX.Element;
|
||||
|
||||
// @alpha
|
||||
export interface TranslationMessages<
|
||||
TId extends string = string,
|
||||
TMessages extends {
|
||||
[key in string]: string;
|
||||
} = {
|
||||
[key in string]: string;
|
||||
},
|
||||
TFull extends boolean = boolean,
|
||||
> {
|
||||
// (undocumented)
|
||||
$$type: '@backstage/TranslationMessages';
|
||||
full: TFull;
|
||||
id: TId;
|
||||
messages: TMessages;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export interface TranslationMessagesOptions<
|
||||
TId extends string,
|
||||
TMessages extends {
|
||||
[key in string]: string;
|
||||
},
|
||||
TFull extends boolean,
|
||||
> {
|
||||
// (undocumented)
|
||||
full?: TFull;
|
||||
// (undocumented)
|
||||
messages: false extends TFull
|
||||
? {
|
||||
[key in keyof TMessages]?: string | null;
|
||||
}
|
||||
: {
|
||||
[key in keyof TMessages]: string | null;
|
||||
};
|
||||
// (undocumented)
|
||||
ref: TranslationRef_2<TId, TMessages>;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type TranslationOptions<
|
||||
Messages extends Record<keyof Messages, string> = Record<string, string>,
|
||||
> = Messages;
|
||||
export interface TranslationOptions {}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface TranslationRef<
|
||||
Messages extends Record<keyof Messages, string> = Record<string, string>,
|
||||
TId extends string = string,
|
||||
TMessages extends {
|
||||
[key in string]: string;
|
||||
} = {
|
||||
[key in string]: string;
|
||||
},
|
||||
> {
|
||||
// (undocumented)
|
||||
getDefaultMessages(): Messages;
|
||||
$$type: '@backstage/TranslationRef';
|
||||
// (undocumented)
|
||||
getId(): string;
|
||||
id: TId;
|
||||
// (undocumented)
|
||||
getLazyResources():
|
||||
| Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: Messages;
|
||||
}>
|
||||
>
|
||||
| undefined;
|
||||
// (undocumented)
|
||||
getResources(): Record<string, Messages> | undefined;
|
||||
T: TMessages;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface TranslationRefConfig<
|
||||
Messages extends Record<keyof Messages, string>,
|
||||
export interface TranslationRefOptions<
|
||||
TId extends string,
|
||||
TMessages extends {
|
||||
[key in string]: string;
|
||||
},
|
||||
TTranslations extends {
|
||||
[language in string]: () => Promise<{
|
||||
default: {
|
||||
[key in keyof TMessages]: string | null;
|
||||
};
|
||||
}>;
|
||||
},
|
||||
> {
|
||||
// (undocumented)
|
||||
id: string;
|
||||
id: TId;
|
||||
// (undocumented)
|
||||
lazyResources?: Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: Messages;
|
||||
}>
|
||||
>;
|
||||
messages: TMessages;
|
||||
// (undocumented)
|
||||
messages: Messages;
|
||||
// (undocumented)
|
||||
resources?: Record<string, Messages>;
|
||||
translations?: TTranslations;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export class TranslationRefImpl<Messages extends Record<keyof Messages, string>>
|
||||
implements TranslationRef<Messages>
|
||||
{
|
||||
export interface TranslationResource<TId extends string = string> {
|
||||
// (undocumented)
|
||||
static create<Messages extends Record<keyof Messages, string>>(
|
||||
config: TranslationRefConfig<Messages>,
|
||||
): TranslationRefImpl<Messages>;
|
||||
$$type: '@backstage/TranslationResource';
|
||||
// (undocumented)
|
||||
getDefaultMessages(): Messages;
|
||||
id: TId;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface TranslationResourceOptions<
|
||||
TId extends string,
|
||||
TMessages extends {
|
||||
[key in string]: string;
|
||||
},
|
||||
TTranslations extends {
|
||||
[language in string]: () => Promise<{
|
||||
default:
|
||||
| TranslationMessages_2<TId>
|
||||
| {
|
||||
[key in keyof TMessages]: string | null;
|
||||
};
|
||||
}>;
|
||||
},
|
||||
> {
|
||||
// (undocumented)
|
||||
getId(): string;
|
||||
ref: TranslationRef_2<TId, TMessages>;
|
||||
// (undocumented)
|
||||
getLazyResources():
|
||||
| Record<
|
||||
string,
|
||||
() => Promise<{
|
||||
messages: Messages;
|
||||
}>
|
||||
>
|
||||
| undefined;
|
||||
// (undocumented)
|
||||
getResources(): Record<string, Messages> | undefined;
|
||||
// (undocumented)
|
||||
toString(): string;
|
||||
translations: TTranslations;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
@@ -116,13 +195,15 @@ export function usePluginOptions<
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const useTranslationRef: <
|
||||
Messages extends Record<keyof Messages, string>,
|
||||
TMessages extends {
|
||||
[x: string]: string;
|
||||
},
|
||||
>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
) => <Tkey extends keyof Messages>(
|
||||
key: Tkey,
|
||||
translationRef: TranslationRef<string, TMessages>,
|
||||
) => <TKey extends keyof TMessages & string>(
|
||||
key: TKey,
|
||||
options?: TranslationOptions,
|
||||
) => Messages[Tkey];
|
||||
) => TMessages[TKey];
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -22,14 +22,12 @@ import { ApiRef, createApiRef } from '@backstage/core-plugin-api';
|
||||
export type AppTranslationApi = {
|
||||
getI18n(): i18n;
|
||||
|
||||
addResourcesByRef<Messages extends Record<string, string>>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
): void;
|
||||
addResource(resource: TranslationRef): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const appTranslationApiRef: ApiRef<AppTranslationApi> = createApiRef({
|
||||
id: 'core.apptranslation',
|
||||
id: 'core.translation',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 { createTranslationMessages } from './TranslationMessages';
|
||||
import { createTranslationRef } from './TranslationRef';
|
||||
|
||||
const ref = createTranslationRef({
|
||||
id: 'counting',
|
||||
messages: {
|
||||
one: 'one',
|
||||
two: 'two',
|
||||
three: 'three',
|
||||
} as const,
|
||||
});
|
||||
|
||||
describe('createTranslationMessages', () => {
|
||||
it('should create a partial message set', () => {
|
||||
expect(
|
||||
createTranslationMessages({
|
||||
ref,
|
||||
messages: {
|
||||
one: 'uno',
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/TranslationMessages',
|
||||
id: 'counting',
|
||||
full: false,
|
||||
messages: {
|
||||
one: 'uno',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a full message set', () => {
|
||||
expect(
|
||||
createTranslationMessages({
|
||||
ref,
|
||||
full: true,
|
||||
messages: {
|
||||
one: 'uno',
|
||||
two: 'dos',
|
||||
three: null,
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/TranslationMessages',
|
||||
id: 'counting',
|
||||
full: true,
|
||||
messages: {
|
||||
one: 'uno',
|
||||
two: 'dos',
|
||||
three: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should require all messages in a full set', () => {
|
||||
expect(
|
||||
createTranslationMessages({
|
||||
ref,
|
||||
full: true,
|
||||
// @ts-expect-error
|
||||
messages: {
|
||||
one: 'uno',
|
||||
two: 'dos',
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/TranslationMessages',
|
||||
id: 'counting',
|
||||
full: true,
|
||||
messages: {
|
||||
one: 'uno',
|
||||
two: 'dos',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Represents a collection of messages to be provided for a given translation ref.
|
||||
*
|
||||
* @alpha
|
||||
* @remarks
|
||||
*
|
||||
* This collection of messages can either be used directly as an override for the
|
||||
* default messages, or it can be used to provide translations for a language by
|
||||
* by being referenced by a {@link TranslationResource}.
|
||||
*/
|
||||
export interface TranslationMessages<
|
||||
TId extends string = string,
|
||||
TMessages extends { [key in string]: string } = { [key in string]: string },
|
||||
TFull extends boolean = boolean,
|
||||
> {
|
||||
$$type: '@backstage/TranslationMessages';
|
||||
/** The ID of the translation ref that these messages are for */
|
||||
id: TId;
|
||||
/** Whether or not these messages override all known messages */
|
||||
full: TFull;
|
||||
/** The messages provided for the given translation ref */
|
||||
messages: TMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@link createTranslationMessages}.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface TranslationMessagesOptions<
|
||||
TId extends string,
|
||||
TMessages extends { [key in string]: string },
|
||||
TFull extends boolean,
|
||||
> {
|
||||
ref: TranslationRef<TId, TMessages>;
|
||||
|
||||
full?: TFull;
|
||||
|
||||
messages: false extends TFull
|
||||
? { [key in keyof TMessages]?: string | null }
|
||||
: { [key in keyof TMessages]: string | null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a collection of messages for a given translation ref.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export function createTranslationMessages<
|
||||
TId extends string,
|
||||
TMessages extends { [key in string]: string },
|
||||
TFull extends boolean,
|
||||
>(
|
||||
options: TranslationMessagesOptions<TId, TMessages, TFull>,
|
||||
): TranslationMessages<TId, TMessages, TFull> {
|
||||
return {
|
||||
$$type: '@backstage/TranslationMessages',
|
||||
id: options.ref.id,
|
||||
full: Boolean(options.full) as TFull,
|
||||
messages: options.messages as TMessages,
|
||||
};
|
||||
}
|
||||
@@ -13,48 +13,61 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { TranslationRefImpl, createTranslationRef } from './TranslationRef';
|
||||
import {
|
||||
createTranslationRef,
|
||||
toInternalTranslationRef,
|
||||
} from './TranslationRef';
|
||||
import { toInternalTranslationResource } from './TranslationResource';
|
||||
|
||||
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',
|
||||
const ref = createTranslationRef({
|
||||
id: 'test',
|
||||
messages: { key: 'value' },
|
||||
};
|
||||
});
|
||||
|
||||
const translationRef = createTranslationRef(config);
|
||||
const internalRef = toInternalTranslationRef(ref);
|
||||
|
||||
expect(translationRef.getId()).toBe('testId');
|
||||
expect(translationRef.getDefaultMessages()).toEqual({ key: 'value' });
|
||||
expect(internalRef.$$type).toBe('@backstage/TranslationRef');
|
||||
expect(internalRef.version).toBe('v1');
|
||||
expect(internalRef.id).toBe('test');
|
||||
expect(internalRef.getDefaultMessages()).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
it('should get lazy resources', async () => {
|
||||
const config = {
|
||||
id: 'testId',
|
||||
it('should be created with lazy translations', async () => {
|
||||
const ref = createTranslationRef({
|
||||
id: 'test',
|
||||
messages: { key: 'value' },
|
||||
lazyResources: {
|
||||
en: () => Promise.resolve({ messages: { key: 'value' } }),
|
||||
translations: {
|
||||
de: () => Promise.resolve({ default: { key: 'other-value' } }),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const translationRef = TranslationRefImpl.create(config);
|
||||
const internalRef = toInternalTranslationRef(ref);
|
||||
|
||||
const lazyResources = translationRef.getLazyResources();
|
||||
expect(internalRef.$$type).toBe('@backstage/TranslationRef');
|
||||
expect(internalRef.version).toBe('v1');
|
||||
expect(internalRef.id).toBe('test');
|
||||
expect(internalRef.getDefaultMessages()).toEqual({ key: 'value' });
|
||||
|
||||
const messages = await lazyResources?.en();
|
||||
|
||||
expect(messages).toEqual({ messages: { key: 'value' } });
|
||||
const internalResource = toInternalTranslationResource(
|
||||
internalRef.getDefaultResource()!,
|
||||
);
|
||||
expect(internalResource).toEqual({
|
||||
$$type: '@backstage/TranslationResource',
|
||||
version: 'v1',
|
||||
id: 'test',
|
||||
resources: [
|
||||
{
|
||||
language: 'de',
|
||||
loader: expect.any(Function),
|
||||
},
|
||||
],
|
||||
});
|
||||
await expect(internalResource.resources[0].loader()).resolves.toEqual({
|
||||
messages: {
|
||||
key: 'other-value',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,48 +14,131 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TranslationRef, TranslationRefConfig } from './types';
|
||||
import {
|
||||
createTranslationResource,
|
||||
TranslationResource,
|
||||
} from './TranslationResource';
|
||||
|
||||
/** @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);
|
||||
}
|
||||
export interface TranslationRef<
|
||||
TId extends string = string,
|
||||
TMessages extends { [key in string]: string } = { [key in string]: string },
|
||||
> {
|
||||
$$type: '@backstage/TranslationRef';
|
||||
|
||||
getId() {
|
||||
return this.config.id;
|
||||
}
|
||||
id: TId;
|
||||
|
||||
getDefaultMessages(): Messages {
|
||||
return this.config.messages;
|
||||
}
|
||||
T: TMessages;
|
||||
}
|
||||
|
||||
getLazyResources():
|
||||
| Record<string, () => Promise<{ messages: Messages }>>
|
||||
| undefined {
|
||||
return this.config.lazyResources;
|
||||
}
|
||||
/** @internal */
|
||||
type AnyMessages = { [key in string]: string };
|
||||
|
||||
getResources(): Record<string, Messages> | undefined {
|
||||
return this.config.resources;
|
||||
}
|
||||
/** @internal */
|
||||
export interface InternalTranslationRef<
|
||||
TId extends string = string,
|
||||
TMessages extends { [key in string]: string } = { [key in string]: string },
|
||||
> extends TranslationRef<TId, TMessages> {
|
||||
version: 'v1';
|
||||
|
||||
toString() {
|
||||
return `TranslationRef(${this.getId()})`;
|
||||
}
|
||||
getDefaultMessages(): AnyMessages;
|
||||
|
||||
private constructor(
|
||||
private readonly config: TranslationRefConfig<Messages>,
|
||||
) {}
|
||||
getDefaultResource(): TranslationResource | undefined;
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export const createTranslationRef = <
|
||||
Messages extends Record<keyof Messages, string> = {},
|
||||
export interface TranslationRefOptions<
|
||||
TId extends string,
|
||||
TMessages extends { [key in string]: string },
|
||||
TTranslations extends {
|
||||
[language in string]: () => Promise<{
|
||||
default: { [key in keyof TMessages]: string | null };
|
||||
}>;
|
||||
},
|
||||
> {
|
||||
id: TId;
|
||||
messages: TMessages;
|
||||
translations?: TTranslations;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
class TranslationRefImpl<
|
||||
TId extends string,
|
||||
TMessages extends { [key in string]: string },
|
||||
> implements InternalTranslationRef<TId, TMessages>
|
||||
{
|
||||
#id: TId;
|
||||
#messages: TMessages;
|
||||
#resources: TranslationResource | undefined;
|
||||
|
||||
constructor(options: TranslationRefOptions<TId, TMessages, any>) {
|
||||
this.#id = options.id;
|
||||
this.#messages = options.messages;
|
||||
}
|
||||
|
||||
$$type = '@backstage/TranslationRef' as const;
|
||||
|
||||
version = 'v1' as const;
|
||||
|
||||
get id(): TId {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
get T(): never {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
getDefaultMessages(): AnyMessages {
|
||||
return this.#messages;
|
||||
}
|
||||
|
||||
setDefaultResource(resources: TranslationResource): void {
|
||||
this.#resources = resources;
|
||||
}
|
||||
|
||||
getDefaultResource(): TranslationResource | undefined {
|
||||
return this.#resources;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `TranslationRef{id=${this.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export function createTranslationRef<
|
||||
TId extends string,
|
||||
TMessages extends { [key in string]: string },
|
||||
TTranslations extends {
|
||||
[language in string]: () => Promise<{
|
||||
default: { [key in keyof TMessages]: string | null };
|
||||
}>;
|
||||
},
|
||||
>(
|
||||
config: TranslationRefConfig<Messages>,
|
||||
): TranslationRef<Messages> => TranslationRefImpl.create(config);
|
||||
config: TranslationRefOptions<TId, TMessages, TTranslations>,
|
||||
): TranslationRef<TId, TMessages> {
|
||||
const ref = new TranslationRefImpl(config);
|
||||
if (config.translations) {
|
||||
ref.setDefaultResource(
|
||||
createTranslationResource({
|
||||
ref,
|
||||
translations: config.translations as any,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalTranslationRef(
|
||||
ref: TranslationRef,
|
||||
): InternalTranslationRef {
|
||||
const r = ref as InternalTranslationRef;
|
||||
if (r.$$type !== '@backstage/TranslationRef') {
|
||||
throw new Error(`Invalid translation ref, bad type '${r.$$type}'`);
|
||||
}
|
||||
if (r.version !== 'v1') {
|
||||
throw new Error(`Invalid translation ref, bad version '${r.version}'`);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 {
|
||||
createTranslationResource,
|
||||
toInternalTranslationResource,
|
||||
} from './TranslationResource';
|
||||
import { countingTranslationRef } from './__fixtures__/refs';
|
||||
|
||||
describe('createTranslationResource', () => {
|
||||
it('should create a simple resource', async () => {
|
||||
const resource = createTranslationResource({
|
||||
ref: countingTranslationRef,
|
||||
translations: {
|
||||
sv: () =>
|
||||
Promise.resolve({
|
||||
default: {
|
||||
one: 'ett',
|
||||
two: 'två',
|
||||
three: 'tre',
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(toInternalTranslationResource(resource)).toEqual({
|
||||
$$type: '@backstage/TranslationResource',
|
||||
version: 'v1',
|
||||
id: 'counting',
|
||||
resources: [
|
||||
{
|
||||
language: 'sv',
|
||||
loader: expect.any(Function),
|
||||
},
|
||||
],
|
||||
});
|
||||
await expect(
|
||||
toInternalTranslationResource(resource).resources[0].loader(),
|
||||
).resolves.toEqual({
|
||||
messages: {
|
||||
one: 'ett',
|
||||
two: 'två',
|
||||
three: 'tre',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a resource with lazy loaded messages', async () => {
|
||||
const resource = createTranslationResource({
|
||||
ref: countingTranslationRef,
|
||||
translations: {
|
||||
de: () => import('./__fixtures__/counting-de'),
|
||||
sv: () => import('./__fixtures__/counting-sv.json'),
|
||||
// @ts-expect-error
|
||||
deBad: () => import('./__fixtures__/fruits-de.json'),
|
||||
// @ts-expect-error
|
||||
svBad: () => import('./__fixtures__/fruits-sv'),
|
||||
},
|
||||
});
|
||||
expect(toInternalTranslationResource(resource)).toEqual({
|
||||
$$type: '@backstage/TranslationResource',
|
||||
version: 'v1',
|
||||
id: 'counting',
|
||||
resources: [
|
||||
{
|
||||
language: 'de',
|
||||
loader: expect.any(Function),
|
||||
},
|
||||
{
|
||||
language: 'sv',
|
||||
loader: expect.any(Function),
|
||||
},
|
||||
{
|
||||
language: 'deBad',
|
||||
loader: expect.any(Function),
|
||||
},
|
||||
{
|
||||
language: 'svBad',
|
||||
loader: expect.any(Function),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
toInternalTranslationResource(resource).resources[0].loader(),
|
||||
).resolves.toEqual({
|
||||
messages: {
|
||||
one: 'eins',
|
||||
two: 'zwei',
|
||||
three: 'polizei',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
toInternalTranslationResource(resource).resources[1].loader(),
|
||||
).resolves.toEqual({
|
||||
messages: {
|
||||
one: 'ett',
|
||||
two: 'två',
|
||||
three: 'tre',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 {
|
||||
TranslationMessages,
|
||||
TranslationRef,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
/** @alpha */
|
||||
export interface TranslationResource<TId extends string = string> {
|
||||
$$type: '@backstage/TranslationResource';
|
||||
id: TId;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalTranslationResource<TId extends string = string>
|
||||
extends TranslationResource<TId> {
|
||||
version: 'v1';
|
||||
resources: Array<{
|
||||
language: string;
|
||||
loader(): Promise<{ messages: { [key in string]: string | null } }>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalTranslationResource<TId extends string>(
|
||||
resource: TranslationResource<TId>,
|
||||
): InternalTranslationResource<TId> {
|
||||
const r = resource as InternalTranslationResource<TId>;
|
||||
if (r.$$type !== '@backstage/TranslationResource') {
|
||||
throw new Error(`Invalid translation resource, bad type '${r.$$type}'`);
|
||||
}
|
||||
if (r.version !== 'v1') {
|
||||
throw new Error(`Invalid translation resource, bad version '${r.version}'`);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export interface TranslationResourceOptions<
|
||||
TId extends string,
|
||||
TMessages extends { [key in string]: string },
|
||||
TTranslations extends {
|
||||
[language in string]: () => Promise<{
|
||||
default:
|
||||
| TranslationMessages<TId>
|
||||
| { [key in keyof TMessages]: string | null };
|
||||
}>;
|
||||
},
|
||||
> {
|
||||
ref: TranslationRef<TId, TMessages>;
|
||||
|
||||
translations: TTranslations;
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export function createTranslationResource<
|
||||
TId extends string,
|
||||
TMessages extends { [key in string]: string },
|
||||
TTranslations extends {
|
||||
[language in string]: () => Promise<{
|
||||
default:
|
||||
| TranslationMessages<TId>
|
||||
| { [key in keyof TMessages]: string | null };
|
||||
}>;
|
||||
},
|
||||
>(
|
||||
options: TranslationResourceOptions<TId, TMessages, TTranslations>,
|
||||
): TranslationResource<TId> {
|
||||
return {
|
||||
$$type: '@backstage/TranslationResource',
|
||||
version: 'v1',
|
||||
id: options.ref.id,
|
||||
resources: Object.entries(options.translations).map(
|
||||
([language, loader]) => ({
|
||||
language,
|
||||
loader: () =>
|
||||
loader().then(m => {
|
||||
const value = m.default;
|
||||
return {
|
||||
messages:
|
||||
value?.$$type === '@backstage/TranslationMessages'
|
||||
? value.messages
|
||||
: value,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
),
|
||||
} as InternalTranslationResource<TId>;
|
||||
}
|
||||
@@ -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 { createTranslationMessages } from '../TranslationMessages';
|
||||
import { countingTranslationRef } from './refs';
|
||||
|
||||
export default createTranslationMessages({
|
||||
ref: countingTranslationRef,
|
||||
messages: {
|
||||
one: 'eins',
|
||||
two: 'zwei',
|
||||
three: 'polizei',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"one": "ett",
|
||||
"two": "två",
|
||||
"three": "tre"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"apple": "apfel",
|
||||
"orange": "apfelsine"
|
||||
}
|
||||
+11
-2
@@ -13,5 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './apis/implementations/AppTranslationApi';
|
||||
export * from './app/TranslationResource';
|
||||
|
||||
import { createTranslationMessages } from '../TranslationMessages';
|
||||
import { fruitsTranslationRef } from './refs';
|
||||
|
||||
export default createTranslationMessages({
|
||||
ref: fruitsTranslationRef,
|
||||
messages: {
|
||||
apple: 'äpple',
|
||||
orange: 'apelsin',
|
||||
},
|
||||
});
|
||||
+19
-16
@@ -14,21 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { createTranslationRef } from '../TranslationRef';
|
||||
|
||||
/** @alpha */
|
||||
export type TranslationMessages<T> = T extends TranslationRef<infer R>
|
||||
? Partial<R>
|
||||
: never;
|
||||
export const countingTranslationRef = createTranslationRef({
|
||||
id: 'counting',
|
||||
messages: {
|
||||
one: 'one',
|
||||
two: 'two',
|
||||
three: 'three',
|
||||
},
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export function createTranslationResource<T extends TranslationRef>(options: {
|
||||
ref: T;
|
||||
messages?: Record<string, TranslationMessages<T>>;
|
||||
lazyMessages: Record<
|
||||
string,
|
||||
() => Promise<{ messages: TranslationMessages<T> }>
|
||||
>;
|
||||
}) {
|
||||
return options;
|
||||
}
|
||||
export const fruitsTranslationRef = createTranslationRef({
|
||||
id: 'fruits',
|
||||
messages: {
|
||||
apple: 'apple',
|
||||
orange: 'orange',
|
||||
},
|
||||
translations: {
|
||||
de: () => import('./fruits-de.json'),
|
||||
},
|
||||
});
|
||||
@@ -14,6 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './TranslationRef';
|
||||
export * from './types';
|
||||
export * from './useTranslationRef';
|
||||
export {
|
||||
type TranslationMessages,
|
||||
type TranslationMessagesOptions,
|
||||
createTranslationMessages,
|
||||
} from './TranslationMessages';
|
||||
export {
|
||||
type TranslationResource,
|
||||
type TranslationResourceOptions,
|
||||
createTranslationResource,
|
||||
} from './TranslationResource';
|
||||
export {
|
||||
type TranslationRef,
|
||||
type TranslationRefOptions,
|
||||
createTranslationRef,
|
||||
} from './TranslationRef';
|
||||
export {
|
||||
type TranslationOptions,
|
||||
useTranslationRef,
|
||||
} from './useTranslationRef';
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
@@ -51,7 +51,7 @@ describe('useTranslationRef', () => {
|
||||
};
|
||||
|
||||
(useApi as jest.Mock).mockReturnValue({
|
||||
addResourcesByRef: jest.fn(),
|
||||
addResource: jest.fn(),
|
||||
});
|
||||
|
||||
(useTranslation as jest.Mock).mockReturnValue(i18nMock);
|
||||
|
||||
@@ -16,26 +16,34 @@
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { TranslationOptions, TranslationRef } from './types';
|
||||
import { useApi } from '../apis';
|
||||
import { appTranslationApiRef } from '../apis/alpha';
|
||||
import { toInternalTranslationRef, TranslationRef } from './TranslationRef';
|
||||
|
||||
/** @alpha */
|
||||
export interface TranslationOptions {
|
||||
/* no options supported for now */
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export const useTranslationRef = <
|
||||
Messages extends Record<keyof Messages, string>,
|
||||
TMessages extends { [key in string]: string },
|
||||
>(
|
||||
translationRef: TranslationRef<Messages>,
|
||||
translationRef: TranslationRef<string, TMessages>,
|
||||
) => {
|
||||
const appTranslationApi = useApi(appTranslationApiRef);
|
||||
const translationApi = useApi(appTranslationApiRef);
|
||||
|
||||
appTranslationApi.addResourcesByRef(translationRef);
|
||||
const internalRef = toInternalTranslationRef(translationRef);
|
||||
translationApi.addResource(translationRef);
|
||||
|
||||
const { t } = useTranslation(translationRef.getId());
|
||||
const { t } = useTranslation(internalRef.id, {
|
||||
useSuspense: process.env.NODE_ENV !== 'test',
|
||||
});
|
||||
|
||||
const defaulteMessage = translationRef.getDefaultMessages();
|
||||
const defaultMessages = internalRef.getDefaultMessages();
|
||||
|
||||
return <Tkey extends keyof Messages>(
|
||||
key: Tkey,
|
||||
return <TKey extends keyof TMessages & string>(
|
||||
key: TKey,
|
||||
options?: TranslationOptions,
|
||||
): Messages[Tkey] => t(key as string, defaulteMessage[key], options);
|
||||
): TMessages[TKey] => t(key, defaultMessages[key], options);
|
||||
};
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const adrTranslationRef: TranslationRef<{
|
||||
readonly content_header_title: 'Architecture Decision Records';
|
||||
readonly failed_to_fetch: 'Failed to fetch ADRs';
|
||||
readonly no_adrs: 'No ADRs found';
|
||||
}>;
|
||||
export const adrTranslationRef: TranslationRef<
|
||||
'adr',
|
||||
{
|
||||
readonly content_header_title: 'Architecture Decision Records';
|
||||
readonly failed_to_fetch: 'Failed to fetch ADRs';
|
||||
readonly no_adrs: 'No ADRs found';
|
||||
}
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -6,21 +6,24 @@
|
||||
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;
|
||||
}>;
|
||||
export const userSettingsTranslationRef: TranslationRef<
|
||||
'user-settings',
|
||||
{
|
||||
readonly language: 'Language';
|
||||
readonly change_the_language: 'Change the language';
|
||||
readonly theme: 'Theme';
|
||||
readonly theme_light: 'Light';
|
||||
readonly theme_dark: 'Dark';
|
||||
readonly theme_auto: 'Auto';
|
||||
readonly change_the_theme_mode: 'Change the theme mode';
|
||||
readonly select_theme_light: 'Select light';
|
||||
readonly select_theme_dark: 'Select dark';
|
||||
readonly select_theme_auto: 'Select Auto Theme';
|
||||
readonly select_theme_custom: 'Select {{custom}}';
|
||||
readonly lng: '{{language}}';
|
||||
readonly select_lng: 'Select language {{language}}';
|
||||
}
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import { AppTheme, appThemeApiRef } from '@backstage/core-plugin-api';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import {
|
||||
renderWithEffects,
|
||||
TestApiRegistry,
|
||||
@@ -28,7 +27,6 @@ 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',
|
||||
@@ -41,11 +39,6 @@ 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]),
|
||||
@@ -54,15 +47,6 @@ 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(
|
||||
|
||||
@@ -33,5 +33,5 @@ export const userSettingsTranslationRef = createTranslationRef({
|
||||
select_theme_custom: 'Select {{custom}}',
|
||||
lng: '{{language}}',
|
||||
select_lng: 'Select language {{language}}',
|
||||
},
|
||||
} as const,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user