Skip to content

Commit

Permalink
Merge pull request #648 from kevinkasper/feat/nested_translations
Browse files Browse the repository at this point in the history
Enable nested translations
  • Loading branch information
FlorianRappl authored Nov 29, 2023
2 parents 07d38da + 5d82381 commit c39d667
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 10 deletions.
16 changes: 16 additions & 0 deletions src/plugins/piral-translate/src/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ describe('Create Localize API', () => {
expect(result).toEqual('bár');
});

it('createApi can translate from local-global translations using the current language and passed nested translations', () => {
const config = {
language: 'en'
};
const api = (createLocaleApi(setupLocalizer(config))(context) as any)();
api.setTranslations({
en: {
header: {
title: 'Hello world'
}
}
});
const result = api.translate('header.title');
expect(result).toBe('Hello world');
});

it('createApi falls back to standard string if language is not found', () => {
const config = {
language: 'en',
Expand Down
11 changes: 6 additions & 5 deletions src/plugins/piral-translate/src/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { PiralPlugin } from 'piral-core';
import { createActions } from './actions';
import { Localizer } from './localize';
import { DefaultPicker } from './default';
import { PiletLocaleApi, LocalizationMessages, Localizable, PiralSelectLanguageEvent } from './types';
import { PiletLocaleApi, LocalizationMessages, Localizable, PiralSelectLanguageEvent, NestedLocalizationMessages } from './types';
import { flattenTranslations } from './flatten-translations';

export interface TranslationFallback {
(key: string, language: string): string;
Expand All @@ -22,7 +23,7 @@ export interface LocaleConfig {
* Sets the default (global) localization messages.
* @default {}
*/
messages?: LocalizationMessages;
messages?: LocalizationMessages | NestedLocalizationMessages;
/**
* Sets the default language to use.
*/
Expand Down Expand Up @@ -72,8 +73,8 @@ export function createLocaleApi(localizer: Localizable = setupLocalizer()): Pira
let localTranslations: LocalizationMessages = {};

return {
addTranslations(messages: LocalizationMessages[], isOverriding: boolean = true) {
const messagesToMerge: LocalizationMessages[] = messages;
addTranslations(messages, isOverriding = true) {
const messagesToMerge = messages;

if (isOverriding) {
messagesToMerge.unshift(localizer.messages);
Expand All @@ -100,7 +101,7 @@ export function createLocaleApi(localizer: Localizable = setupLocalizer()): Pira
return selected;
},
setTranslations(messages) {
localTranslations = messages;
localTranslations = flattenTranslations(messages);
},
getTranslations() {
return localTranslations;
Expand Down
56 changes: 56 additions & 0 deletions src/plugins/piral-translate/src/flatten-translations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @vitest-environment jsdom
*/
import { describe, it, expect } from 'vitest';
import { flattenTranslations } from './flatten-translations';

describe('Flatten translations', () => {
it('flattenTranslations can handle flat keys', () => {
const messages = {
en: {
key: 'value'
},
fr: {
key: 'value (fr)'
}
};
const flatMessages = flattenTranslations(messages);
expect(flatMessages.fr.key).toEqual('value (fr)');
});

it('flattenTranslations can handle flat dot keys', () => {
const messages = {
en: {
'header.title': 'Hello world'
}
};
const flatMessages = flattenTranslations(messages);
expect(flatMessages.en['header.title']).toBe('Hello world');
});

it('flattenTranslations can handle nested keys', () => {
const messages = {
en: {
header: {
title: 'Hello world'
}
}
};
const flatMessages = flattenTranslations(messages);
expect(flatMessages.en['header.title']).toBe('Hello world');
});

it('flattenTranslations can handle nested keys with multiple depth levels', () => {
const messages = {
en: {
header: {
title: {
subtitle: 'Hello world'
}
}
}
};
const flatMessages = flattenTranslations(messages);
expect(flatMessages.en['header.title.subtitle']).toBe('Hello world');
});
});
46 changes: 46 additions & 0 deletions src/plugins/piral-translate/src/flatten-translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { LocalizationMessages, NestedLocalizationMessages } from './types';

export function flattenTranslations(messages: LocalizationMessages | NestedLocalizationMessages): LocalizationMessages {
return Object.fromEntries(
Object
.entries(messages)
.map(([ language, translations ]) => {
return [
language,
flat(translations)
]
})
);
}

function flat(source: Record<string, unknown>): Record<string, string> {
const target: Record<string, string> = {};

flatten(source, target);

return target;
}

function flatten(source: any, target: Record<string, string>, prop = '') {
if (typeof source === 'string') {
target[prop] = source;

return;
}

if (typeof source === 'object' && source !== null) {
Object
.keys(source)
.forEach((key) => {
flatten(
source[key],
target,
prop
? `${prop}.${key}`
: key
);
});

return;
}
}
10 changes: 10 additions & 0 deletions src/plugins/piral-translate/src/localize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const messages = {
en: {
hi: 'hello',
greeting: 'Hi {{name}}, welcome back',
header: {
title: 'Hello world'
}
},
de: {
hi: 'hallo',
Expand Down Expand Up @@ -76,4 +79,11 @@ describe('Localize Module', () => {
const result = localizer.localizeGlobal('greeting', { name: undefined });
expect(result).toBe('Hi , welcome back');
});

it('localizeGlobal translates from global translations using passed nested translations', () => {
const localizer = new Localizer(messages, 'en');
const result = localizer.localizeGlobal('header.title');

expect(result).toBe('Hello world');
});
});
11 changes: 8 additions & 3 deletions src/plugins/piral-translate/src/localize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LocalizationMessages, Localizable } from './types';
import { LocalizationMessages, Localizable, NestedLocalizationMessages } from './types';
import { flattenTranslations } from './flatten-translations';

function defaultFallback(key: string, language: string): string {
if (process.env.NODE_ENV === 'production') {
Expand All @@ -20,15 +21,19 @@ function formatMessage<T extends object>(message: string, variables: T): string
}

export class Localizer implements Localizable {
public messages: LocalizationMessages;

/**
* Creates a new instance of a localizer.
*/
constructor(
public messages: LocalizationMessages,
messages: LocalizationMessages | NestedLocalizationMessages,
public language: string,
public languages: Array<string>,
private fallback = defaultFallback,
) {}
) {
this.messages = flattenTranslations(messages);
}

/**
* Localizes the given key via the global translations.
Expand Down
18 changes: 16 additions & 2 deletions src/plugins/piral-translate/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ export interface Translations {
[tag: string]: string;
}

export interface NestedTranslations {
/**
* The available wordings (tag to translation or nested translations).
*/
[tag: string]: string | NestedTranslations;
}

export interface LanguageLoader {
(language: string, current: LanguageData): Promise<LanguageData>;
}
Expand All @@ -119,6 +126,13 @@ export interface LocalizationMessages {
[lang: string]: Translations;
}

export interface NestedLocalizationMessages {
/**
* The available languages (lang to wordings or nested wordings).
*/
[lang: string]: NestedTranslations;
}

export interface PiletLocaleApi {
/**
* Adds a list of translations to the existing translations.
Expand All @@ -129,7 +143,7 @@ export interface PiletLocaleApi {
* @param messagesList The list of messages that extend the existing translations
* @param [isOverriding=true] Indicates whether the new translations overwrite the existing translations
*/
addTranslations(messagesList: LocalizationMessages[], isOverriding?: boolean): void;
addTranslations(messagesList: (LocalizationMessages | NestedLocalizationMessages)[], isOverriding?: boolean): void;
/**
* Gets the currently selected language directly.
*/
Expand All @@ -151,7 +165,7 @@ export interface PiletLocaleApi {
* The translations will be exclusively used for retrieving translations for the pilet.
* @param messages The messages to use as translation basis.
*/
setTranslations(messages: LocalizationMessages): void;
setTranslations(messages: LocalizationMessages | NestedLocalizationMessages): void;
/**
* Gets the currently provided translations by the pilet.
*/
Expand Down

0 comments on commit c39d667

Please sign in to comment.