Skip to content

Commit

Permalink
AB-768 add service error translator library
Browse files Browse the repository at this point in the history
  • Loading branch information
yanief committed Sep 3, 2019
1 parent 1bb20f0 commit 21a98ba
Show file tree
Hide file tree
Showing 15 changed files with 900 additions and 778 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fixed validatePersonName regex to not allow double white space as separator
- Change some validate functions that has `invalidOption` error, to throw error on `invalidOption` instead of returning it.
- Update regex for UUID
- Added Justice service error translator library
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Justice JS Common Utils

Common Javascript utilities for Justice Platform, including:
- Input Validation
- Service Error Translator

And fully containerized development

Expand Down
20 changes: 17 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "justice-js-common-utils",
"version": "0.6.5",
"version": "0.7.0",
"description": "AccelByte's Justice Common Javascript Utilities",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",
Expand All @@ -11,6 +11,7 @@
"scripts": {
"describe": "npm-scripts-info",
"build": "run-s clean && run-p build:*",
"build:i18n": "node scripts/i18nbuilder",
"build:main": "tsc -p tsconfig.json",
"build:module": "tsc -p tsconfig.module.json",
"fix": "run-s fix:*",
Expand All @@ -20,7 +21,7 @@
"test:lint": "tslint --project . && prettier \"src/**/*.ts\" --list-different",
"test:unit": "NODE_ENV=test jest --config \"config/loadJestConfig.js\" --coverage --verbose --detectOpenHandles --forceExit",
"test:watch": "NODE_ENV=test jest --config \"config/loadJestConfig.js\" --coverage --verbose --watchAll",
"watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:watch\"",
"watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:watch\" \"build:i18n\" ",
"doc": "run-s doc:html && npx open-cli build/docs/index.html",
"doc:html": "npx typedoc src/ --exclude **/*.test.ts --target ES6 --mode file --out build/docs",
"doc:json": "npx typedoc src/ --exclude **/*.test.ts --target ES6 --mode file --json build/docs/typedoc.json",
Expand All @@ -41,17 +42,30 @@
"engines": {
"node": ">=8.9"
},
"dependencies": {
"peerDependencies": {
"flat": "^4.1.0",
"i18next": "^17.0.7",
"react": "^16.8.6",
"react-i18next": "^10.11.4",
"validator": "^11.1.0"
},
"devDependencies": {
"flat": "^4.1.0",
"i18next": "^17.0.7",
"react": "^16.8.6",
"react-i18next": "^10.11.4",
"validator": "^11.1.0",
"@bitjson/npm-scripts-info": "^1.0.0",
"@types/flat": "^0.0.28",
"@types/jest": "^24.0.15",
"@types/node": "^12.6.8",
"@types/react": "^16.8.24",
"@types/validator": "^10.11.2",
"husky": "^3.0.1",
"i18next-scanner": "^2.10.2",
"jest": "^24.8.0",
"jest-cli": "^24.8.0",
"json5": "^2.1.0",
"npm-run-all": "^4.1.5",
"object-assign": "^4.1.1",
"open-cli": "^5.0.0",
Expand Down
144 changes: 144 additions & 0 deletions scripts/i18nbuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright (c) 2018-2019 AccelByte Inc. All Rights Reserved.
* This is licensed software from AccelByte Inc, for limitations
* and restrictions contact your company contract manager.
*/

const fs = require("fs");
const path = require("path");
const flatten = require("flat");
const { Parser } = require("i18next-scanner");
const JSON5 = require("json5");

const ts = require("typescript");

function getTranslationFromFile(filePath) {
let error;
let result;
try {
const parser = new Parser({
func: { list: ["i18next.t", "i18n.t", "t", "_t", "translation"] },
});
const content = fs.readFileSync(filePath, "utf8");
const compiled = ts.transpileModule(content, {
compilerOptions: {
target: "es2018",
},
fileName: path.basename(filePath),
}).outputText;
parser.parseFuncFromString(compiled).parseTransFromString(compiled);
result = flatten(parser.get().en.translation);
} catch (err) {
console.error(err);
error = err;
}
return { error, result };
}

function getAllTranslations(files) {
let translations = {};
files.forEach(filePath => {
const { err, result } = getTranslationFromFile(filePath);
if (err) console.error(err);
if (!result) console.warn(`Warning: cannot parse ${filePath}`);
if (result) {
translations = Object.assign(translations, flatten(result));
}
});
return flatten(translations, { safe: true });
}

function getOldTranslationsMap(languages, directory) {
const oldTranslationsMap = {};
languages.forEach(lang => {
try {
const fileContent = fs.readFileSync(
path.resolve(directory, `${lang}.json`),
"utf8"
);
oldTranslationsMap[lang] = flatten(JSON5.parse(fileContent), {
safe: true,
});
} catch (error) {
oldTranslationsMap[lang] = {};
}
});
return oldTranslationsMap;
}

function getUnusedKeysFromOldTranslations(oldTranslation, currentTranslation) {
const keys = Object.keys(currentTranslation).reduce(
(keys, translationKey) => {
keys.delete(translationKey);
return keys;
},
new Set(Object.keys(oldTranslation))
);
keys.delete("_unused");
return Array.from(keys);
}

function sortTranslationKeys(translation) {
const keys = Object.keys(translation);
keys.sort();
return keys.reduce((sorted, key) => {
sorted[key] = translation[key];
return sorted;
}, {});
}

function writeTranslationMap(oldTranslationsMap, translationsMap, directory) {
Object.entries(translationsMap).forEach(([lang, translation]) => {
const oldJSON = JSON.stringify(
sortTranslationKeys(oldTranslationsMap[lang] || {}),
null,
2
);
const newJSON = JSON.stringify(sortTranslationKeys(translation), null, 2);
if (oldJSON !== newJSON) {
fs.writeFileSync(
path.resolve(directory, `${lang}.json`),
newJSON,
"utf8"
);
}
});
}

function startBuild({ languages, files, directory }) {
const translations = getAllTranslations(files);
const oldTranslationsMap = getOldTranslationsMap(languages, directory);
const newTranslationMap = {};
languages.forEach(lang => {
const oldTranslation = oldTranslationsMap[lang] || {};
const unusedKeys = getUnusedKeysFromOldTranslations(
oldTranslation,
translations
);
const newTranslation = Object.assign({}, translations, oldTranslation, {
_unused: unusedKeys.length > 0 ? unusedKeys : undefined,
});
newTranslationMap[lang] = newTranslation;
});
writeTranslationMap(oldTranslationsMap, newTranslationMap, directory);
}

function run({ files, languages, directory }) {
if (languages.length > 0) {
startBuild({
languages,
files,
directory,
});
} else {
console.warn("Language not yet set");
}
}

run({
files: [
path.resolve(__dirname, "../src/lib/service-error-translator/service-error-translator.tsx")
],
languages: ["en-US"],
directory: path.resolve(__dirname, "../src/lib/service-error-translator/translations")
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
*/

export * from "./lib/input-validation";
export * from "./lib/service-error-translator";
3 changes: 1 addition & 2 deletions src/lib/input-validation/constant/numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
* and restrictions contact your company contract manager.
*/

export const MAX_SAFE_INTEGER = 9007199254740991;
export const JAVA_MAX_INT = 2147483647;
export const MAX_INTEGER = 2147483647;
export const MAX_LONG_TEXT_LENGTH = 2000;
export const MAX_SHORT_TEXT_LENGTH = 256;
export const MAX_DISPLAY_NAME_LENGTH = 48;
41 changes: 41 additions & 0 deletions src/lib/service-error-translator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Service Error Translator

## Overview

Service Error Translator is a library that can be used to turn Justice standard error codes into
user-friendly error message. It contains a React component that accepts a standard error response
from Justice backend services.

## Usage Example

```ts
import {
ServiceErrorTranslator,
} from "justice-js-common-utils"

class Component extends React.Component {
// Declare validation as class property
constructor(){
// set username input value as state
this.state = ({
errorMessage: null,
})
}

async fetchUsers = () => {
try {
const response = await api.fetchUsers();
// ...
} catch (error) {
// assuming error is an object with properties ErrorCode and ErrorMessage
this.setState({
errorMessage: <ServiceErrorTranslator error={error} />
});
}
}

render(){
// ...
}
}
```
5 changes: 5 additions & 0 deletions src/lib/service-error-translator/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"languageCodes": ["en-US"],
"defaultLanguage": "en-US",
"fallbackLanguage": "en-US"
}
42 changes: 42 additions & 0 deletions src/lib/service-error-translator/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2018-2019 AccelByte Inc. All Rights Reserved
* This is licensed software from AccelByte Inc, for limitations
* and restrictions contact your company contract manager.
*/

import flatten from "flat";
import i18next, { Resource } from "i18next";
import { initReactI18next } from "react-i18next";
import config from "./config.json";

const loadedLanguages: { [key: string]: string } = {
"en-US": "enUS",
};
const availableLanguageCodes = config.languageCodes;
const translationResource = availableLanguageCodes.reduce((resources: Resource, languageCode: string) => {
// eslint-disable-next-line no-param-reassign
resources[languageCode] = {
// Loading unflattened resource
translation: flatten.unflatten(loadedLanguages[languageCode]),
};
return resources;
}, {});

// @ts-ignore
export const i18nInstance = i18next.use(initReactI18next).createInstance(
{
lng: config.defaultLanguage,
fallbackLng: config.fallbackLanguage,
preload: availableLanguageCodes,
resources: translationResource,
initImmediate: false,
debug: process.env.NODE_ENV === "development",
},
// tslint:disable-next-line no-empty
() => {}
); // Do not remove the callback. It will break the i18n

// tslint:disable-next-line no-any
export function t(key: string, options?: any) {
return i18nInstance.t(key, options);
}
7 changes: 7 additions & 0 deletions src/lib/service-error-translator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright (c) 2019. AccelByte Inc. All Rights Reserved
* This is licensed software from AccelByte Inc, for limitations
* and restrictions contact your company contract manager.
*/

export * from "./service-error-translator";
44 changes: 44 additions & 0 deletions src/lib/service-error-translator/service-error-translator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2019. AccelByte Inc. All Rights Reserved
* This is licensed software from AccelByte Inc, for limitations
* and restrictions contact your company contract manager.
*/

import * as React from "react";
import { Trans } from "react-i18next";

interface ServiceError {
ErrorCode: number,
ErrorMessage: string,
}

interface ServiceErrorProps {
error: ServiceError;
}

// tslint:disable-next-line no-any
const isValidServiceError = (error: any): error is ServiceError => {
return typeof error === "object" && typeof error.ErrorCode === "number" && typeof error.ErrorMessage === "string";
}

export const ServiceErrorTranslator = (props: ServiceErrorProps): React.ReactNode => {
if (isValidServiceError(props.error) && props.error.ErrorCode in serviceErrorTranslationMap) {
return serviceErrorTranslationMap[props.error.ErrorCode];
}
return (
<Trans i18nKey="serviceError.unknown">
Failed to complete the request
</Trans>
);
};

const serviceErrorTranslationMap: { [key: string]: React.ReactNode } = Object.freeze({
1014002:
<Trans i18nKey="serviceError.1014002">
User already exist
</Trans>,
1014047:
<Trans i18nKey="serviceError.1014047">
Failed to create User. Date of birth does not meet the age requirement.
</Trans>,
});
5 changes: 5 additions & 0 deletions src/lib/service-error-translator/translations/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"serviceError.1014002": "User already exist",
"serviceError.1014047": "Failed to create User. Date of birth does not meet the age requirement.",
"serviceError.unknown": "Failed to complete the request"
}
Loading

0 comments on commit 21a98ba

Please sign in to comment.