Merge branch 'master' of https://github.com/backstage/backstage into feat/allow-inspect-override
Signed-off-by: jrwpatterson <jrwpatterson@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-graphiql': patch
|
||||
---
|
||||
|
||||
Add support for using the FetchApi
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/backend-tasks': patch
|
||||
---
|
||||
|
||||
When starting a task that existed before, with a faster schedule than it
|
||||
previously had, the task will now correctly obey the faster schedule
|
||||
immediately. Before this fix, the new schedule was only obeyed after the next
|
||||
pending (according to the old schedule) run had completed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Minor internal tweak to handle `classnames` update
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': minor
|
||||
---
|
||||
|
||||
Slight change to the `PreparePullRequestFormProps`, because of an update to `react-hook-form`.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"mode": "exit",
|
||||
"tag": "next",
|
||||
"initialVersions": {
|
||||
"example-app": "0.2.86",
|
||||
|
||||
@@ -62,7 +62,7 @@ steps:
|
||||
- **Bundling** - Combines a package and all of its dependencies into a
|
||||
production-ready bundle
|
||||
|
||||
These steps are generally kept isolated form each other, with each step focusing
|
||||
These steps are generally kept isolated from each other, with each step focusing
|
||||
on its specific task. For example, we do not do linting or type checking
|
||||
together with the building or bundling. This is so that we can provide more
|
||||
flexibility and avoid duplicate work, improving performance. It is strongly
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Search extension for Azure Cognitive Search
|
||||
author: AP Communications
|
||||
authorUrl: https://www.ap-com.co.jp/en/
|
||||
category: Search
|
||||
description: Search extension plugin to create backstage search indexes into Azure Cognitive Search.
|
||||
documentation: https://github.com/ap-communications/platt-backstage-plugin/tree/main/plugins/search-backend-module-cognitive-search
|
||||
npmPackageName: '@platt/plugin-search-backend-module-cognitive-search'
|
||||
addedDate: '2023-09-13'
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Veecode GitHub Workflows
|
||||
author: Veecode Platform
|
||||
authorUrl: https://github.com/veecode-platform
|
||||
category: CI/CD
|
||||
description: The Github Workflows plugin provides an alternative for manually triggering GitHub workflows from within your Backstage component.
|
||||
documentation: https://github.com/veecode-platform/platform-backstage-plugins/tree/master/plugins/github-workflows
|
||||
iconUrl: https://veecode-platform.github.io/support/imgs/logo_2.svg
|
||||
npmPackageName: '@veecode-platform/backstage-plugin-github-workflows'
|
||||
tags:
|
||||
- ci
|
||||
- cd
|
||||
- github
|
||||
- workflows
|
||||
addedDate: '2023-09-12'
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
errorApiRef,
|
||||
fetchApiRef,
|
||||
githubAuthApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { AuthProxyDiscoveryApi } from './AuthProxyDiscoveryApi';
|
||||
@@ -53,18 +54,24 @@ export const apis: AnyApiFactory[] = [
|
||||
|
||||
createApiFactory({
|
||||
api: graphQlBrowseApiRef,
|
||||
deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef },
|
||||
factory: ({ errorApi, githubAuthApi }) =>
|
||||
deps: {
|
||||
errorApi: errorApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
githubAuthApi: githubAuthApiRef,
|
||||
},
|
||||
factory: ({ errorApi, fetchApi, githubAuthApi }) =>
|
||||
GraphQLEndpoints.from([
|
||||
GraphQLEndpoints.create({
|
||||
id: 'gitlab',
|
||||
title: 'GitLab',
|
||||
url: 'https://gitlab.com/api/graphql',
|
||||
fetchApi,
|
||||
}),
|
||||
GraphQLEndpoints.github({
|
||||
id: 'github',
|
||||
title: 'GitHub',
|
||||
errorApi,
|
||||
fetchApi,
|
||||
githubAuthApi,
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -330,6 +330,48 @@ describe('TaskWorker', () => {
|
||||
const before = fn1.mock.calls.length;
|
||||
await promise2;
|
||||
expect(fn1.mock.calls.length).toBeGreaterThan(before);
|
||||
|
||||
await knex.destroy();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'next_run_start_at is always the min between schedule changes, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
await migrateBackendTasks(knex);
|
||||
|
||||
const fn = jest.fn(
|
||||
async () => new Promise<void>(resolve => setTimeout(resolve, 50)),
|
||||
);
|
||||
const settings: TaskSettingsV2 = {
|
||||
version: 2,
|
||||
cadence: '*/15 * * * *',
|
||||
initialDelayDuration: 'PT2M',
|
||||
timeoutAfterDuration: 'PT1M',
|
||||
};
|
||||
|
||||
const worker = new TaskWorker('task99', fn, knex, logger);
|
||||
await worker.persistTask(settings);
|
||||
const row1 = (await knex<DbTasksRow>(DB_TASKS_TABLE))[0];
|
||||
|
||||
const settings2 = {
|
||||
...settings,
|
||||
cadence: '*/2 * * * *',
|
||||
initialDelayDuration: 'PT1M',
|
||||
};
|
||||
await worker.persistTask(settings2);
|
||||
const row2 = (await knex<DbTasksRow>(DB_TASKS_TABLE))[0];
|
||||
|
||||
expect(row2.next_run_start_at).not.toStrictEqual(row1.next_run_start_at);
|
||||
|
||||
const settings3 = { ...settings };
|
||||
await worker.persistTask(settings3);
|
||||
const row3 = (await knex<DbTasksRow>(DB_TASKS_TABLE))[0];
|
||||
|
||||
expect(row3.next_run_start_at).toStrictEqual(row2.next_run_start_at);
|
||||
|
||||
await knex.destroy();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -192,14 +192,36 @@ export class TaskWorker {
|
||||
|
||||
// It's OK if the task already exists; if it does, just replace its
|
||||
// settings with the new value and start the loop as usual.
|
||||
const settingsJson = JSON.stringify(settings);
|
||||
await this.knex<DbTasksRow>(DB_TASKS_TABLE)
|
||||
.insert({
|
||||
id: this.taskId,
|
||||
settings_json: JSON.stringify(settings),
|
||||
settings_json: settingsJson,
|
||||
next_run_start_at: startAt,
|
||||
})
|
||||
.onConflict('id')
|
||||
.merge(['settings_json']);
|
||||
.merge(
|
||||
this.knex.client.config.client.includes('mysql')
|
||||
? {
|
||||
settings_json: settingsJson,
|
||||
next_run_start_at: this.knex.raw(
|
||||
`CASE WHEN ?? < ?? THEN ?? ELSE ?? END`,
|
||||
[startAt, 'next_run_start_at', startAt, 'next_run_start_at'],
|
||||
),
|
||||
}
|
||||
: {
|
||||
settings_json: this.knex.ref('excluded.settings_json'),
|
||||
next_run_start_at: this.knex.raw(
|
||||
`CASE WHEN ?? < ?? THEN ?? ELSE ?? END`,
|
||||
[
|
||||
'excluded.next_run_start_at',
|
||||
`${DB_TASKS_TABLE}.next_run_start_at`,
|
||||
'excluded.next_run_start_at',
|
||||
`${DB_TASKS_TABLE}.next_run_start_at`,
|
||||
],
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,9 @@ import { rest } from 'msw';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { Lockfile } from '../../lib/versioning/Lockfile';
|
||||
|
||||
// Avoid mutating the global http(s) agent used in other tests
|
||||
jest.mock('global-agent/bootstrap', () => {});
|
||||
|
||||
// Remove log coloring to simplify log matching
|
||||
jest.mock('chalk', () => ({
|
||||
red: (str: string) => str,
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -351,7 +351,7 @@ export const WorkaroundNavLink = React.forwardRef<
|
||||
aria-current={ariaCurrent}
|
||||
style={{ ...style, ...(isActive ? activeStyle : undefined) }}
|
||||
className={classnames([
|
||||
className,
|
||||
typeof className !== 'function' ? className : undefined,
|
||||
isActive ? activeClassName : undefined,
|
||||
])}
|
||||
/>
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -238,7 +238,7 @@ export type PreparePullRequestFormProps<
|
||||
UseFormReturn<TFieldValues>,
|
||||
'formState' | 'register' | 'control' | 'setValue'
|
||||
> & {
|
||||
values: UnpackNestedValue<TFieldValues>;
|
||||
values: TFieldValues;
|
||||
},
|
||||
) => React_2.ReactNode;
|
||||
};
|
||||
|
||||
+1
-2
@@ -18,7 +18,6 @@ import React from 'react';
|
||||
import {
|
||||
FormProvider,
|
||||
SubmitHandler,
|
||||
UnpackNestedValue,
|
||||
useForm,
|
||||
UseFormProps,
|
||||
UseFormReturn,
|
||||
@@ -39,7 +38,7 @@ export type PreparePullRequestFormProps<
|
||||
UseFormReturn<TFieldValues>,
|
||||
'formState' | 'register' | 'control' | 'setValue'
|
||||
> & {
|
||||
values: UnpackNestedValue<TFieldValues>;
|
||||
values: TFieldValues;
|
||||
},
|
||||
) => React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { ErrorApi } from '@backstage/core-plugin-api';
|
||||
import { FetchApi } from '@backstage/core-plugin-api';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { OAuthApi } from '@backstage/core-plugin-api';
|
||||
@@ -23,6 +24,7 @@ export type EndpointConfig = {
|
||||
headers?: {
|
||||
[name in string]: string;
|
||||
};
|
||||
fetchApi?: FetchApi;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -31,6 +33,7 @@ export type GithubEndpointConfig = {
|
||||
title: string;
|
||||
url?: string;
|
||||
errorApi?: ErrorApi;
|
||||
fetchApi?: FetchApi;
|
||||
githubAuthApi: OAuthApi;
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { GraphQLBrowseApi, GraphQLEndpoint } from './types';
|
||||
import { ErrorApi, OAuthApi } from '@backstage/core-plugin-api';
|
||||
import { ErrorApi, FetchApi, OAuthApi } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* Helper for generic http endpoints
|
||||
@@ -31,6 +31,10 @@ export type EndpointConfig = {
|
||||
method?: 'POST';
|
||||
// Defaults to setting Content-Type to application/json
|
||||
headers?: { [name in string]: string };
|
||||
/**
|
||||
* Fetch API to use instead of browser fetch()
|
||||
*/
|
||||
fetchApi?: FetchApi;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
@@ -45,6 +49,10 @@ export type GithubEndpointConfig = {
|
||||
* Errors will be posted to the ErrorApi if it is provided.
|
||||
*/
|
||||
errorApi?: ErrorApi;
|
||||
/**
|
||||
* Fetch API to use instead of browser fetch()
|
||||
*/
|
||||
fetchApi?: FetchApi;
|
||||
/**
|
||||
* GitHub Auth API used to authenticate requests.
|
||||
*/
|
||||
@@ -55,7 +63,7 @@ export type GithubEndpointConfig = {
|
||||
export class GraphQLEndpoints implements GraphQLBrowseApi {
|
||||
// Create a support
|
||||
static create(config: EndpointConfig): GraphQLEndpoint {
|
||||
const { id, title, url, method = 'POST' } = config;
|
||||
const { id, title, url, method = 'POST', fetchApi } = config;
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
@@ -66,7 +74,7 @@ export class GraphQLEndpoints implements GraphQLBrowseApi {
|
||||
...config.headers,
|
||||
...options.headers,
|
||||
};
|
||||
const res = await fetch(await url, {
|
||||
const res = await (fetchApi?.fetch ?? fetch)(await url, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
@@ -88,6 +96,7 @@ export class GraphQLEndpoints implements GraphQLBrowseApi {
|
||||
title,
|
||||
url = 'https://api.github.com/graphql',
|
||||
errorApi,
|
||||
fetchApi,
|
||||
githubAuthApi,
|
||||
} = config;
|
||||
type ResponseBody = {
|
||||
@@ -101,7 +110,7 @@ export class GraphQLEndpoints implements GraphQLBrowseApi {
|
||||
let retried = false;
|
||||
|
||||
const doRequest = async (): Promise<any> => {
|
||||
const res = await fetch(url, {
|
||||
const res = await (fetchApi?.fetch ?? fetch)(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import 'buffer';
|
||||
|
||||
import 'buffer';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { errorHandler, getVoidLogger } from '@backstage/backend-common';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
@@ -44,7 +45,6 @@ import {
|
||||
KubernetesProxy,
|
||||
} from './KubernetesProxy';
|
||||
import fetch from 'cross-fetch';
|
||||
import mockFs from 'mock-fs';
|
||||
|
||||
import type { Request } from 'express';
|
||||
|
||||
@@ -680,15 +680,7 @@ describe('KubernetesProxy', () => {
|
||||
httpsRequest.mockClear();
|
||||
});
|
||||
describe('should pass the exact response from Kubernetes using the CA file', () => {
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should trust contents of specified caFile', async () => {
|
||||
mockFs({
|
||||
'/path/to/ca.crt': 'MOCKCA',
|
||||
});
|
||||
|
||||
const apiResponse = {
|
||||
kind: 'APIVersions',
|
||||
versions: ['v1'],
|
||||
@@ -706,7 +698,7 @@ describe('KubernetesProxy', () => {
|
||||
url: 'https://localhost:9999',
|
||||
serviceAccountToken: '',
|
||||
authProvider: 'serviceAccount',
|
||||
caFile: '/path/to/ca.crt',
|
||||
caFile: resolvePath(__dirname, '__fixtures__/mock-ca.crt'),
|
||||
},
|
||||
] as ClusterDetails[]);
|
||||
|
||||
@@ -736,7 +728,7 @@ describe('KubernetesProxy', () => {
|
||||
|
||||
expect(httpsRequest).toHaveBeenCalledTimes(1);
|
||||
const [[{ ca }]] = httpsRequest.mock.calls;
|
||||
expect(ca).toEqual('MOCKCA');
|
||||
expect(ca).toMatch('MOCKCA');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
MOCKCA
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -21802,9 +21802,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"classnames@npm:*, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "classnames@npm:2.3.1"
|
||||
checksum: 14db8889d56c267a591f08b0834989fe542d47fac659af5a539e110cc4266694e8de86e4e3bbd271157dbd831361310a8293e0167141e80b0f03a0f175c80960
|
||||
version: 2.3.2
|
||||
resolution: "classnames@npm:2.3.2"
|
||||
checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -37373,11 +37373,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"react-hook-form@npm:^7.12.2, react-hook-form@npm:^7.13.0":
|
||||
version: 7.32.2
|
||||
resolution: "react-hook-form@npm:7.32.2"
|
||||
version: 7.46.1
|
||||
resolution: "react-hook-form@npm:7.46.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
checksum: abe8ba146c313d206f99a01260457bc72af03b2c73d7bd4be5af11598140ec45ef1ef37bac64b80c4dbc298d6aa21af4caa259a133f305a04ddf583faa18e7d0
|
||||
checksum: 9c11ba454ce5b2a16e7499f2ca710e0c0cff227f39689b8e7985902f27f99bfc7a2c49f145ef228528764cf8286bf6101fc8a0f5094ce652eb31cab1684518d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user