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

Implement array type translations #12

Merged
merged 4 commits into from
May 4, 2021
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
All notable changes to this project will be documented in this file.

## [Unreleased]
### Added
- Support for translation arrays.

## [2.2.1] - 2021-05-03
### Changed
Expand Down
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,37 @@ const ExampleComponent = () => {
export default ExampleComponent;
```

##### Array translations
```javascript
import React from "react";
import { useLittera } from "react-littera";

const translations = {
greetings: [
{
de_DE: "Guten Tag",
en_US: "Good morning"
},
{
de_DE: "Hallo",
en_US: "Hello"
},
]
};

const ExampleComponent = () => {
// Obtain our translated object.
const translated = useLittera(translations);

// Get the translated strings from the array.
const varTranslation = translated[0]; // => Good morning

return <button onClick={handleLocaleChange}>{varTranslation}</button>;
};

export default ExampleComponent;
```

#### HOC Example

```javascript
Expand Down Expand Up @@ -255,6 +286,18 @@ This hook exposes following methods:
})
```

#### ITranslationsArr
`ITranslation[]`

```javascript
[
{
de_DE: "Beispiel",
en_US: "Example"
},
]
```

#### ITranslations
`{ [key: string]: ITranslation | ITranslationVarFn }`

Expand All @@ -267,17 +310,28 @@ This hook exposes following methods:
hello: (name) => ({
de_DE: `Hallo ${name}`,
en_US: `Hello ${name}`
})
}),
greetings: [
{
de_DE: "Guten Tag",
en_US: "Good morning"
},
{
de_DE: "Hallo",
en_US: "Hello"
},
]
}
```

#### ITranslated
`{ [key: string]: string | (...args: (string | number)[]) => string }`
`{ [key: string]: string | ((...args: (string | number)[]) => string) | string[] }`

```javascript
{
simple: "Simple",
hello: (name) => "Hello Mike" // Run this function to get variable translation.
hello: (name) => "Hello Mike", // Run this function to get variable translation.
greetings: [ "Good morning", "Hello" ]
}
```

Expand Down
13 changes: 11 additions & 2 deletions src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,22 @@ import { ITranslations, TSetLocale, TValidateLocale, ITranslated, TTranslationsA
* hello: (name: string) => ({
* en_US: `Hello ${name}`,
* de_DE: `Hallo ${name}`
* })
* }),
* news: [
* {
* en_US: "The spaghetti code monster ate our homework.",
* de_DE: "Das Spaghetti-Code-Monster aß unsere Hausaufgaben."
* }
* ]
* }
*
* const YourComponent = () => {
* const translated = useLittera(translations);
*
* return <h2>{translated.example} - {translated.hello("Mike")}</h2>
* return <>
* <h2>{translated.example} - {translated.hello("Mike")}</h2>
* <p>{translated.news[0]}</p>
* </>
* }
* @returns {ITranslated}
*/
Expand Down
28 changes: 27 additions & 1 deletion src/utils/methods.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ITranslations, ITranslationVarFn } from "../../types";
import { ITranslation, ITranslations, ITranslationsArr, ITranslationVarFn } from "../../types";

export const localePattern = /[a-z]{2}_[A-Z]{2}/gi;

Expand Down Expand Up @@ -42,6 +42,8 @@ export const tryParseLocale = (locale: string) => {
export const reportMissing = <T>(translations: ITranslations<T>, locales: string[]) => {
Object.keys(translations).forEach(key => {
if(typeof translations[key] === "function") return; // TODO: Detect missing translations for variable functions.
if(translations[key] instanceof Array) return; // TODO: Detect missing translations for arrays.

locales.forEach(locale => {

if (typeof translations[key][locale] !== "string")
Expand All @@ -59,6 +61,30 @@ export const reportMissing = <T>(translations: ITranslations<T>, locales: string
*/
export const lookForMissingKeys = reportMissing;

/**
* Checks if value is a translation object.
* @param value
* @returns
*/
export function isTranslation(value: unknown): value is ITranslation {
return typeof (value as ITranslation) === "object";
}

/**
* Checks if value is a variable function.
* @param value
* @returns
*/
export function isVariableFunction(value: unknown): value is ITranslationVarFn {
return typeof (value as ITranslationVarFn) === "function";
}

/**
* Checks if value is a translations array.
* @param value
* @returns
*/
export function isTransArrayFunction(value: unknown): value is ITranslationsArr {
return (value as ITranslationsArr) instanceof Array &&
!(value as ITranslationsArr).find(v => !isTranslation(v))
}
26 changes: 22 additions & 4 deletions src/utils/translate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ITranslated, ISingleTranslated } from "../../types";
import { isVariableFunction } from "./methods";
import { isTransArrayFunction, isVariableFunction } from "./methods";

/**
* Returns object with translated values based on locale.
Expand All @@ -18,12 +18,19 @@ import { isVariableFunction } from "./methods";
* en_US: `Hello ${name}`,
* de_DE: `Hallo ${name}`
* })
* slogans: [
* {
* en_US: "Welcome to the show",
* de_DE: "Willkommen in der Show"
* }
* ]
* }
*
*
* const translated = translate(translations, "de_DE");
*
* translated.example // => "Beispiel"
* translated.hello("Mike") // => "Hallo Mike"
* translated.slogans[0] // => "Welcome to the show"
* @returns {ITranslated}
*/
export function translate<T, K extends keyof T>(
Expand Down Expand Up @@ -66,17 +73,28 @@ export function translate<T, K extends keyof T>(
* hello: (name: string) => ({
* en_US: `Hello ${name}`,
* de_DE: `Hallo ${name}`
* })
* }),
* slogans: [
* {
* en_US: "Welcome to the show",
* de_DE: "Willkommen in der Show"
* }
* ]
* }
*
* const translatedExample = translateSingle(translations.example, "de_DE");
* const translatedExample = translateSingle(translations.hello("Mike"), "de_DE");
* const translatedHello = translateSingle(translations.hello("Mike"), "de_DE");
* const translatedArr = translateSingle(translations.slogans[], "de_DE");
*
* translatedExample // => "Beispiel"
* translatedHello("Mike") // => "Hallo Mike"
* translatedArr[0] // => "Welcome to the show"
* @returns {ISingleTranslated}
*/
export function translateSingle<T>(translation: T, locale: string) {
if(isTransArrayFunction(translation))
return translation.map(t => translateSingle(t, locale)) as ISingleTranslated<T>;

if (isVariableFunction(translation))
return ((...args: Parameters<typeof translation>) => translation(...args)[locale]) as ISingleTranslated<T>;

Expand Down
52 changes: 44 additions & 8 deletions tests/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ const translationsMock = {
de_DE: "Beispiel",
en_US: "Example",
pl_PL: "Przykład"
}
}

const translationsMockWithVariables = {
},
greeting: (name: string) => ({
de_DE: `Hallo ${name}`,
en_US: `Hello ${name}`,
Expand All @@ -18,7 +15,26 @@ const translationsMockWithVariables = {
de_DE: "Du",
en_US: "You",
pl_PL: "Ty"
}
},
slogans: [
{
en_US: "Welcome to the show",
de_DE: "Willkommen in der Show",
pl_PL: "Witamy w przedstawieniu"
}
],
greetings: [
{
pl_PL: "Dzień dobry",
en_US: "Good morning",
de_DE: "Guten Morgen"
},
{
pl_PL: "Cześć",
en_US: "Hello",
de_DE: "Hallo"
},
]
}

describe('translate', () => {
Expand All @@ -28,16 +44,23 @@ describe('translate', () => {

it("should translate flat translations", () => {
expect(translate(translationsMock, "en_US").example).toBe("Example")
expect(translate(translationsMockWithVariables, "de_DE").you).toBe("Du")
expect(translate(translationsMock, "de_DE").you).toBe("Du")
});

it("should translate flat translations with variables", () => {
const translated = translate(translationsMockWithVariables, "en_US");
const translated = translate(translationsMock, "en_US");

expect(translated.greeting("Mike")).toBe("Hello Mike");
expect(translated.greeting(translated.you)).toBe("Hello You");
});

it("should translate flat translations with arrays", () => {
const translated = translate(translationsMock, "en_US");

expect(translated.slogans[0]).toBe("Welcome to the show");
expect(translated.greetings[1]).toBe("Hello");
});

it("should throw error if translations is invalid type", () => {
const fn = () => {
// @ts-ignore
Expand Down Expand Up @@ -69,8 +92,21 @@ describe('translateSingle', () => {
});

it("should translate flat translation with variables", () => {
const result = translateSingle(translationsMockWithVariables.greeting, "en_US");
const result = translateSingle(translationsMock.greeting, "en_US");

expect(result("Mike")).toBe("Hello Mike");
});

it("should translate flat translation with arrays", () => {
const result = translateSingle(translationsMock.slogans, "en_US");

expect(result[0]).toBe("Welcome to the show");
});

it("should translate flat translation with empty array", () => {
const result = translateSingle([], "en_US");

expect(result !== undefined).toBe(true);
expect(result[0]).toBe(undefined);
});
})
42 changes: 42 additions & 0 deletions tests/hooks.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,39 @@ const mockTranslationsWithVariables = Object.freeze({
})
});

const mockTranslationsArrs = Object.freeze({
hello: (name: string) => ({
de_DE: `Hallo ${name}`,
pl_PL: `Cześć ${name}`,
en_US: `Hello ${name}`
}),
simple: {
de_DE: "Einfach",
pl_PL: "Proste",
en_US: "Simple"
},
slogans: [
{
en_US: "Welcome to the show",
pl_PL: "Witamy w programie"
},
{
en_US: "Welcome back!",
pl_PL: "Witaj spowrotem!"
},
],
greetings: [
{
pl_PL: "Dzień dobry",
en_US: "Good morning"
},
{
pl_PL: "Cześć",
en_US: "Hello"
},
]
})

const mockMissingTranslations = {
simple: {
pl_PL: "Proste",
Expand Down Expand Up @@ -89,6 +122,15 @@ describe("useLittera", () => {
expect(translated.hello(translated.very(translated.simple, "Magic"))).toBe("Cześć Bardzo Proste oraz Magic");
});

it("should return correct translation with arrays", () => {
const render = renderHook(() => useLittera(mockTranslationsArrs), { wrapper });
const translated = render.result.current;

expect(translated.slogans.length).toBe(2);
expect(translated.slogans[0]).toBe("Witamy w programie");
expect(translated.greetings).toStrictEqual([ "Dzień dobry", "Cześć" ]);
});

it("should return correct translation from preset", () => {
const render = renderHook(() => useLittera(mockTranslationsFunc), { wrapper });
const translated = render.result.current;
Expand Down
Loading