core-plugin-api: add support for nested default translation message declarations
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user