Skip to content

Commit

Permalink
feat: add methods for replacing placeholders (#31)
Browse files Browse the repository at this point in the history
* feat: add methods for replacing placeholders

* docs: add new functions to readme and missed jsdoc
  • Loading branch information
ImRodry committed Feb 8, 2022
1 parent d2bd86f commit 442a9c2
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 21 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@typescript-eslint/no-explicit-any": 1,
"@typescript-eslint/explicit-function-return-type": 2,
"@typescript-eslint/no-namespace": 0,
"@typescript-eslint/no-non-null-assertion": "off",
"no-unused-expressions": 2,
"curly": 2,
"semi": 2,
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ Also there are several helper methods.
| `getManifestTimestamp` | Get manifest timestamp of distribution | |
| `listFiles` | List of files in distribution | |
| `listLanguages` | List of project language codes | |
| `getReplacedLanguages` | List of project language codes in the provided format | `format` (placeholder format you want to replace your languages with, e.g. `locale`) |
| `getReplacedFiles` | List of files in distribution with variables replaced with the corresponding language code | |
| `getLanguageObjects` | List of project language objects | |
| `clearStringsCache` | Clear cache of translation strings | |
| `getLanguageMappings` | Get project language mapping | |
| `getCustomLanguages` | Get project custom languages | |
Expand All @@ -163,7 +166,7 @@ const hash = '{distribution_hash}';

const client = new otaClient(hash);

// will return all translation strings for all languages from all json files
// will return all translation strings for all languages from all json files
client.getStrings()
.then(res => {
//get needed translation by language + key
Expand Down Expand Up @@ -224,8 +227,8 @@ If you've found an error in these samples, please [Contact Customer Success Serv

## License
<pre>
The Crowdin OTA JavaScript client is licensed under the MIT License.
See the LICENSE.md file distributed with this work for additional
The Crowdin OTA JavaScript client is licensed under the MIT License.
See the LICENSE.md file distributed with this work for additional
information regarding copyright ownership.

Except as contained in the LICENSE file, the name(s) of the above copyright
Expand Down
65 changes: 64 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { AxiosHttpClient } from './internal/http/axiosClient';
import { includesLanguagePlaceholders, replaceLanguagePlaceholders } from './internal/util/exportPattern';
import {
findLanguageObject,
includesLanguagePlaceholders,
Language,
LanguagePlaceholders,
languagePlaceholders,
replaceLanguagePlaceholders,
} from './internal/util/exportPattern';
import { isJsonFile, mergeDeep } from './internal/util/strings';

export interface ClientConfig {
Expand Down Expand Up @@ -64,11 +71,16 @@ export interface LanguageTranslations {
content: string | any | null;
}

export interface LanguageFiles {
[languageCode: string]: string[];
}

export interface LanguageStrings {
[languageCode: string]: any;
}

export interface CustomLanguage {
name: string;
two_letters_code: string;
three_letters_code: string;
locale: string;
Expand Down Expand Up @@ -148,6 +160,57 @@ export default class OtaClient {
return (await this.manifest).languages;
}

/**
* List of project language codes in the provided format
* @param format The placeholder format you want to replace your languages with
*/
async getReplacedLanguages(format: LanguagePlaceholders): Promise<string[]> {
const [languages, customLanguages] = await Promise.all([
await this.listLanguages(),
await this.getCustomLanguages(),
]);

return languages.map(l => languagePlaceholders[format](findLanguageObject(l, customLanguages?.[l])));
}

/**
* List of files in distribution with variables replaced with the corresponding language code
*/
async getReplacedFiles(): Promise<LanguageFiles> {
const [customLanguages, languageMappings, files, languages] = await Promise.all([
await this.getCustomLanguages(),
await this.getLanguageMappings(),
await this.listFiles(),
await this.listLanguages(),
]);

const result: Record<string, string[]> = {};
await Promise.all(
languages.map(async language => {
result[language] = await Promise.all(
files.map(file =>
replaceLanguagePlaceholders(
file,
language,
languageMappings?.[language],
customLanguages?.[language],
),
),
);
}),
);
return result;
}

/**
* List of project language objects
*/
async getLanguageObjects(): Promise<Language[]> {
const languages = await this.listLanguages();
const customLanguages = await this.getCustomLanguages();
return Promise.all(languages.map(language => findLanguageObject(language, customLanguages?.[language])!));
}

/**
* Language mappings
*/
Expand Down
38 changes: 27 additions & 11 deletions src/internal/util/exportPattern.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CustomLanguage, LanguageMapping } from '../..';

interface Language {
export interface Language {
name: string;
twoLettersCode: string;
threeLettersCode: string;
Expand All @@ -13,7 +13,18 @@ interface Language {

type Mapper<I, O> = (input: I) => O;

const languagePlaceholders: { [placeholder: string]: Mapper<Language, string> } = {
export type LanguagePlaceholders =
| '%language%'
| '%language%'
| '%two_letters_code%'
| '%three_letters_code%'
| '%locale%'
| '%locale_with_underscore%'
| '%android_code%'
| '%osx_code%'
| '%osx_locale%';

export const languagePlaceholders: Record<LanguagePlaceholders, Mapper<Language, string>> = {
'%language%': lang => lang.name,
'%two_letters_code%': lang => lang.twoLettersCode,
'%three_letters_code%': lang => lang.threeLettersCode,
Expand Down Expand Up @@ -2857,16 +2868,11 @@ export function includesLanguagePlaceholders(str: string): boolean {
return Object.keys(languagePlaceholders).some(placeholder => str.includes(placeholder));
}

export function replaceLanguagePlaceholders(
str: string,
languageCode: string,
languageMapping?: LanguageMapping,
customLanguage?: CustomLanguage,
): string {
export function findLanguageObject(languageCode: string, customLanguage?: CustomLanguage): Language {
let language: Language | undefined;
if (customLanguage) {
language = {
name: languageCode,
name: customLanguage.name,
twoLettersCode: customLanguage.two_letters_code,
threeLettersCode: customLanguage.three_letters_code,
locale: customLanguage.locale,
Expand All @@ -2879,16 +2885,26 @@ export function replaceLanguagePlaceholders(
language = languages.find(l => l.osxLocale === languageCode || l.locale === languageCode);
}
if (!language) {
throw new Error(`Unsupported language code : ${languageCode}`);
throw new Error(`Unsupported language code: ${languageCode}`);
}
return language;
}

export function replaceLanguagePlaceholders(
str: string,
languageCode: string,
languageMapping?: LanguageMapping,
customLanguage?: CustomLanguage,
): string {
const language = findLanguageObject(languageCode, customLanguage);
let result = str;
for (const placeholder of Object.keys(languagePlaceholders)) {
if (result.includes(placeholder)) {
const cleanPlaceholder = placeholder.slice(1, -1);
const replaceValue =
languageMapping && languageMapping[cleanPlaceholder]
? languageMapping[cleanPlaceholder]
: languagePlaceholders[placeholder](language);
: languagePlaceholders[placeholder as LanguagePlaceholders](language);
result = result.replace(placeholder, replaceValue);
}
}
Expand Down
56 changes: 50 additions & 6 deletions tests/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import * as nock from 'nock';
import OtaClient, { Manifest } from '../src/index';
import { Language } from '../src/internal/util/exportPattern';

describe('OTA client', () => {
const now = Date.now();
let scope: nock.Scope;
const languageCode = 'uk';
const languageLocale = 'uk-UA';
const languageObject: Language = {
name: 'Ukrainian',
twoLettersCode: 'uk',
threeLettersCode: 'ukr',
locale: 'uk-UA',
androidCode: 'uk-rUA',
osxCode: 'uk.lproj',
osxLocale: 'uk',
};
const hash = 'testHash';
const hashForStrings = 'jsonTestHash';
const client: OtaClient = new OtaClient(hash);
const clientWithJsonFiles: OtaClient = new OtaClient(hashForStrings, { languageCode });
const hashForPlaceholders = 'jsonTestHashForPlaceholders';
const client = new OtaClient(hash);
const clientWithJsonFiles = new OtaClient(hashForStrings, { languageCode });
const clientWithPlaceholders = new OtaClient(hashForPlaceholders);
const fileContent = '"apple","яблуко","","",""';
const filePath = '/folder1/file1.csv';
const filePathWithPlaceholder = '/folder1/%locale%/file1.csv';
const filePathReplacedPlaceholder = '/folder1/uk-UA/file1.csv';
const customLanguageLocale = 'ua';
const manifest: Manifest = {
files: [filePath],
Expand All @@ -22,7 +37,7 @@ describe('OTA client', () => {
locale: customLanguageLocale,
},
},
custom_languages: [],
custom_languages: [] as never[],
/* eslint-enable @typescript-eslint/camelcase */
};
const jsonFilePath1 = '/folder/file1.json';
Expand All @@ -42,8 +57,17 @@ describe('OTA client', () => {
languages: [languageCode],
timestamp: now,
/* eslint-disable @typescript-eslint/camelcase */
language_mapping: [],
custom_languages: [],
language_mapping: [] as never[],
custom_languages: [] as never[],
/* eslint-enable @typescript-eslint/camelcase */
};
const manifestWithPlaceholders: Manifest = {
files: [filePathWithPlaceholder],
languages: [languageCode],
timestamp: now,
/* eslint-disable @typescript-eslint/camelcase */
language_mapping: [] as never[],
custom_languages: [] as never[],
/* eslint-enable @typescript-eslint/camelcase */
};

Expand All @@ -59,7 +83,9 @@ describe('OTA client', () => {
.get(`/${hashForStrings}/content/${languageCode}${jsonFilePath1}?timestamp=${now}`)
.reply(200, jsonFileContent1)
.get(`/${hashForStrings}/content/${languageCode}${jsonFilePath2}?timestamp=${now}`)
.reply(200, jsonFileContent2);
.reply(200, jsonFileContent2)
.get(`/${hashForPlaceholders}/manifest.json`)
.reply(200, manifestWithPlaceholders);
});

afterAll(() => {
Expand Down Expand Up @@ -87,11 +113,29 @@ describe('OTA client', () => {
expect(files).toEqual(manifest.files);
});

it('should return an object with the language ids and an array of files with their formats correctly replaced', async () => {
const replacedFiles = await clientWithPlaceholders.getReplacedFiles();
expect(replacedFiles.uk.length).toEqual(manifest.files.length);
expect(replacedFiles.uk).toEqual([filePathReplacedPlaceholder]);
});

it('should return list of languages from manifest', async () => {
const languages = await client.listLanguages();
expect(languages).toEqual(manifest.languages);
});

it('should return list of languages in the given format', async () => {
const languages = await clientWithPlaceholders.getReplacedLanguages('%locale%');
expect(languages.length).toEqual(manifest.languages.length);
expect(languages).toEqual([languageLocale]);
});

it('should return an array of language objects', async () => {
const languageObjects = await client.getLanguageObjects();
expect(languageObjects.length).toEqual(manifest.languages.length);
expect(languageObjects).toEqual([languageObject]);
});

it('should return language mappings', async () => {
const mappings = (await client.getLanguageMappings()) || {};
expect(mappings).toBeDefined();
Expand Down
1 change: 1 addition & 0 deletions tests/internal/util/exportPattern.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('Export Pattern Util', () => {
locale: 'ua',
};
const customLanguage: CustomLanguage = {
name: 'Test Language',
/*eslint-disable-next-line @typescript-eslint/camelcase*/
two_letters_code: 'tl',
/*eslint-disable-next-line @typescript-eslint/camelcase*/
Expand Down

0 comments on commit 442a9c2

Please sign in to comment.