core-plugin-api: add support for nested default translation message declarations

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-11-24 13:46:42 +01:00
parent 9a0bb13e29
commit 0c93dc37b2
4 changed files with 107 additions and 24 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-plugin-api': patch
---
The `createTranslationRef` function from the `/alpha` subpath can now also accept a nested object structure of default translation messages, which will be flatted using `.` separators.
+7 -11
View File
@@ -39,19 +39,17 @@ export function createTranslationMessages<
// @alpha (undocumented)
export function createTranslationRef<
TId extends string,
const TMessages extends {
[key in string]: string;
},
const TNestedMessages extends AnyNestedMessages,
TTranslations extends {
[language in string]: () => Promise<{
default: {
[key in keyof TMessages]: string | null;
[key in keyof FlattenedMessages<TNestedMessages>]: string | null;
};
}>;
},
>(
config: TranslationRefOptions<TId, TMessages, TTranslations>,
): TranslationRef<TId, TMessages>;
config: TranslationRefOptions<TId, TNestedMessages, TTranslations>,
): TranslationRef<TId, FlattenedMessages<TNestedMessages>>;
// @alpha (undocumented)
export function createTranslationResource<
@@ -169,13 +167,11 @@ export interface TranslationRef<
// @alpha (undocumented)
export interface TranslationRefOptions<
TId extends string,
TMessages extends {
[key in string]: string;
},
TNestedMessages extends AnyNestedMessages,
TTranslations extends {
[language in string]: () => Promise<{
default: {
[key in keyof TMessages]: string | null;
[key in keyof FlattenedMessages<TNestedMessages>]: string | null;
};
}>;
},
@@ -183,7 +179,7 @@ export interface TranslationRefOptions<
// (undocumented)
id: TId;
// (undocumented)
messages: TMessages;
messages: TNestedMessages;
// (undocumented)
translations?: TTranslations;
}
@@ -34,6 +34,40 @@ describe('TranslationRefImpl', () => {
expect(internalRef.getDefaultMessages()).toEqual({ key: 'value' });
});
it('should create a TranslationRef instance with nested messages', () => {
const ref = createTranslationRef({
id: 'test',
messages: {
key: 'value',
'nested.conflict1': 'outer conflict1',
nested: {
key: 'nested value',
key2: 'nested value2',
conflict1: 'inner conflict1',
conflict2: 'inner conflict2',
inner: {
key: 'inner value',
},
},
'nested.conflict2': 'outer conflict2',
},
});
const internalRef = toInternalTranslationRef(ref);
expect(internalRef.$$type).toBe('@backstage/TranslationRef');
expect(internalRef.version).toBe('v1');
expect(internalRef.id).toBe('test');
expect(internalRef.getDefaultMessages()).toEqual({
key: 'value',
'nested.key': 'nested value',
'nested.key2': 'nested value2',
'nested.conflict1': 'inner conflict1',
'nested.inner.key': 'inner value',
'nested.conflict2': 'outer conflict2',
});
});
it('should be created with lazy translations', async () => {
const ref = createTranslationRef({
id: 'test',
@@ -34,6 +34,30 @@ export interface TranslationRef<
/** @internal */
type AnyMessages = { [key in string]: string };
/** @ignore */
type AnyNestedMessages = { [key in string]: AnyNestedMessages | string };
/**
* Flattens a nested message declaration into a flat object with dot-separated keys.
*
* @ignore
*/
type FlattenedMessages<T extends AnyNestedMessages> = {
[K in keyof T]: (
x: T[K] extends infer V
? V extends AnyNestedMessages
? FlattenedMessages<V> extends infer FV
? {
[P in keyof FV as `${K & string}.${P & string}`]: FV[P];
}
: never
: Pick<T, K>
: never,
) => void;
} extends Record<keyof T, (y: infer O) => void>
? { [K in keyof O]: O[K] }
: never;
/** @internal */
export interface InternalTranslationRef<
TId extends string = string,
@@ -49,31 +73,53 @@ export interface InternalTranslationRef<
/** @alpha */
export interface TranslationRefOptions<
TId extends string,
TMessages extends { [key in string]: string },
TNestedMessages extends AnyNestedMessages,
TTranslations extends {
[language in string]: () => Promise<{
default: { [key in keyof TMessages]: string | null };
default: {
[key in keyof FlattenedMessages<TNestedMessages>]: string | null;
};
}>;
},
> {
id: TId;
messages: TMessages;
messages: TNestedMessages;
translations?: TTranslations;
}
function flattenMessages(nested: AnyNestedMessages): AnyMessages {
const entries = new Array<[string, string]>();
function visit(obj: AnyNestedMessages, prefix: string): void {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
entries.push([prefix + key, value]);
} else {
visit(value, `${prefix}${key}.`);
}
}
}
visit(nested, '');
return Object.fromEntries(entries);
}
/** @internal */
class TranslationRefImpl<
TId extends string,
TMessages extends { [key in string]: string },
> implements InternalTranslationRef<TId, TMessages>
TNestedMessages extends AnyNestedMessages,
> implements InternalTranslationRef<TId, FlattenedMessages<TNestedMessages>>
{
#id: TId;
#messages: TMessages;
#messages: FlattenedMessages<TNestedMessages>;
#resources: TranslationResource | undefined;
constructor(options: TranslationRefOptions<TId, TMessages, any>) {
constructor(options: TranslationRefOptions<TId, TNestedMessages, any>) {
this.#id = options.id;
this.#messages = options.messages;
this.#messages = flattenMessages(
options.messages,
) as FlattenedMessages<TNestedMessages>;
}
$$type = '@backstage/TranslationRef' as const;
@@ -108,15 +154,17 @@ class TranslationRefImpl<
/** @alpha */
export function createTranslationRef<
TId extends string,
const TMessages extends { [key in string]: string },
const TNestedMessages extends AnyNestedMessages,
TTranslations extends {
[language in string]: () => Promise<{
default: { [key in keyof TMessages]: string | null };
default: {
[key in keyof FlattenedMessages<TNestedMessages>]: string | null;
};
}>;
},
>(
config: TranslationRefOptions<TId, TMessages, TTranslations>,
): TranslationRef<TId, TMessages> {
config: TranslationRefOptions<TId, TNestedMessages, TTranslations>,
): TranslationRef<TId, FlattenedMessages<TNestedMessages>> {
const ref = new TranslationRefImpl(config);
if (config.translations) {
ref.setDefaultResource(
@@ -132,7 +180,7 @@ export function createTranslationRef<
/** @internal */
export function toInternalTranslationRef<
TId extends string,
TMessages extends { [key in string]: string },
TMessages extends AnyMessages,
>(ref: TranslationRef<TId, TMessages>): InternalTranslationRef<TId, TMessages> {
const r = ref as InternalTranslationRef<TId, TMessages>;
if (r.$$type !== '@backstage/TranslationRef') {