Skip to content

Commit

Permalink
Merge pull request #12 from Assembless/feat/array-translations
Browse files Browse the repository at this point in the history
Implement array type translations
  • Loading branch information
DRFR0ST authored May 4, 2021
2 parents db76d52 + 7f55c30 commit 36d2232
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 23 deletions.
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

0 comments on commit 36d2232

Please sign in to comment.