Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable nested translations #648

Merged
merged 1 commit into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading