Pull request #961: New client dashboard
Merge in DNS/adguard-home from new-client-dashboard to master Squashed commit of the following: commit 7bbd67c1e3d2af62b96bf41bb356cd6b784e473e Merge: 113743a69cd9054cAuthor: Vlad <v.abdulmyanov@adguard.com> Date: Wed Feb 3 16:01:17 2021 +0300 Merge branch 'master' into new-client-dashboard commit 113743a60665e40383d367dc17fa709dc54e4e2e Author: Vlad <v.abdulmyanov@adguard.com> Date: Wed Feb 3 15:45:16 2021 +0300 Remove unneded modal styles commit 04f9d93a9ac17ee046f0d5bedfb2bf5a5e6c0a48 Author: Vlad <v.abdulmyanov@adguard.com> Date: Wed Feb 3 14:19:56 2021 +0300 Consider comments commit 78a96cd8fed8b3e03547e7e45724c23db295f67b Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 18:46:52 2021 +0300 Remove old params for MiniCssExtractPlugin commit 40e5a9b2b1e04036deb70af17f2719eadd0c9c02 Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 18:27:46 2021 +0300 Fix mobile version commit 509cefc308f945b03cafa62bf48257490a0a4be1 Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 18:20:56 2021 +0300 Remove unneeded imports commit d192f39cd2503b8ec942f00ba78fca02cac9fa60 Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 18:20:13 2021 +0300 Finish first version of dashboard commit f82429e53d334874ff7dd0641092ec83c66ab61c Merge: fd91a0a33e0238aaAuthor: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 17:12:59 2021 +0300 Merge branch 'master' into new-client-dashboard commit fd91a0a3d76c2a052a6548232b75d151d6065b88 Author: Vlad <v.abdulmyanov@adguard.com> Date: Mon Feb 1 17:12:27 2021 +0300 wip commit 237679965052d38acfcd6a72d24b2444cc5b3896 Author: Vlad <v.abdulmyanov@adguard.com> Date: Fri Jan 29 11:18:10 2021 +0300 Finish general settings commit 397a7e10efd34a8d31bb175a5a5a7158338388d4 Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 28 19:24:03 2021 +0300 Add General settings page commit 486aaa6f3f9ad66f3a0dcfcccad9a32659767e90 Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 28 14:05:16 2021 +0300 Remove husky commit b895306c0655019ca56ce161e050d83b4e7f5ff1 Merge: a195f1f4154c9c1cAuthor: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 28 14:03:37 2021 +0300 Merge branch 'master' into new-client-dashboard commit a195f1f4d46043d9c53dea08734733f9817b95a0 Merge: c45c5fe9 362f390f Author: Vlad <v.abdulmyanov@adguard.com> Date: Wed Jan 27 15:46:18 2021 +0300 Merge branch 'new-client-dashboard' of ssh://bit.adguard.com:7999/dns/adguard-home into new-client-dashboard commit c45c5fe92e6c5c852bec8f512dc46b4cd513156c Author: Vlad <v.abdulmyanov@adguard.com> Date: Wed Jan 27 15:46:01 2021 +0300 wip commit 362f390fd3dcfca75633a8d30a2e54c3c30b4f3d Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com> Date: Wed Jan 27 15:45:12 2021 +0300 Pull request #949: + client: add setup guide page Merge in DNS/adguard-home from 2554-setup-guide to new-client-dashboard Squashed commit of the following: commit c240d52e9e5d90429f2018fde808f4d04ccec138 Merge: 256f1056 137b88e4 Author: Ildar Kamalov <ik@adguard.com> Date: Wed Jan 27 14:13:52 2021 +0300 Merge branch 'new-client-dashboard' into 2554-setup-guide commit 256f1056770c67339e93275ab6dc7aaf2c10da0b Author: Ildar Kamalov <ik@adguard.com> Date: Wed Jan 27 14:10:45 2021 +0300 + client: add DNS addresses to the setup guide commit 0ecf91275a16ecc0dca23cae2ae209836fc622d2 Author: Ildar Kamalov <ik@adguard.com> Date: Wed Jan 27 14:00:12 2021 +0300 + client: add setup guide tabs commit 137b88e4253af5be32d542adbe74575ef74805c8 Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 21 19:17:58 2021 +0300 Add clients top commit c3318e6932d87fdff5f22d76bee12b49f099129a Merge: 2776276b 021eb22f Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 21 19:15:57 2021 +0300 Merge branch 'new-client-dashboard' of ssh://bit.adguard.com:7999/dns/adguard-home into new-client-dashboard commit 2776276b2e6dc026e1326b02c388fcf7d48d47ff Author: Vlad <v.abdulmyanov@adguard.com> Date: Thu Jan 21 19:15:53 2021 +0300 Add top client info commit 021eb22ff877aec12eb7fab60147a2cc2ddd08b7 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 14:13:54 2021 +0300 Merge: client: add sidebar Squashed commit of the following: commit 6885ba953971e68602889fbb3219221f90265421 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 13:56:55 2021 +0300 add sidebar mask commit f069bfe8cba2b31355e19a51ca00bf774ee9e560 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 13:03:47 2021 +0300 fix store commit 77c8791002887ae022da07dc264d9010576e7bab Merge: d0a6eff6 ea6d54d4 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 13:01:04 2021 +0300 Merge branch 'new-client-dashboard' into 2254-sidebar commit d0a6eff67fd74533d63f5d56382085e98ddbb702 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 12:47:32 2021 +0300 client: remove unused file commit 9d2424477de85503fe41fa00cc1294cb0c0e7dfa Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 12:39:13 2021 +0300 client: header commit 9ddea19c136f15b184caa72d7e82738f7d4f3f1f Merge: 797f1248 b694bb05 Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 10:57:24 2021 +0300 Merge branch 'new-client-dashboard' into 2254-sidebar commit 797f1248df5c1ef8e59c2a9999138f9e05a7adaa Author: Ildar Kamalov <ik@adguard.com> Date: Thu Jan 21 10:51:57 2021 +0300 client: sidebar ... and 14 more commits
This commit is contained in:
@@ -1,82 +0,0 @@
|
||||
import translator from './lib/translator';
|
||||
import { AllowedValues } from './lib/formatter';
|
||||
import { getForm, GenericLocales, AvailableLocales } from './lib/plural';
|
||||
|
||||
type ExternalFormater = (data: any) => any;
|
||||
|
||||
class Translator<Locale extends GenericLocales[keyof GenericLocales], Formater = any> {
|
||||
private _currentLocale: Locale;
|
||||
|
||||
private _formatter: ExternalFormater = (data: string[]) => data.join('');
|
||||
|
||||
get currentLocale() {
|
||||
return this._currentLocale;
|
||||
}
|
||||
|
||||
defaultLocale: Locale;
|
||||
|
||||
updateTranslator = (locale: Locale) => {
|
||||
return new Translator<Locale, Formater>(
|
||||
this.defaultLocale,
|
||||
this.messages,
|
||||
locale,
|
||||
this._formatter,
|
||||
);
|
||||
};
|
||||
|
||||
messages: Record<Locale, { [id: string]: string }>;
|
||||
|
||||
constructor(
|
||||
defaultLocale: Locale,
|
||||
messages: Record<Locale, { [id: string]: string }>,
|
||||
currentLocale?: Locale,
|
||||
formatter?: ExternalFormater,
|
||||
) {
|
||||
this.defaultLocale = defaultLocale;
|
||||
this._currentLocale = currentLocale ?? defaultLocale;
|
||||
this.messages = messages;
|
||||
|
||||
if (formatter) {
|
||||
this._formatter = formatter;
|
||||
}
|
||||
}
|
||||
|
||||
public getMessage(
|
||||
id: string,
|
||||
params: AllowedValues<Formater> = {},
|
||||
): string {
|
||||
const str = this.messages[this._currentLocale][id]
|
||||
|| this.messages[this.defaultLocale][id]
|
||||
|| id;
|
||||
|
||||
const tranlation = translator<Formater>(str, params);
|
||||
return this._formatter(tranlation);
|
||||
}
|
||||
|
||||
public getPlural(
|
||||
id: string,
|
||||
number: number,
|
||||
params: AllowedValues<Formater> = {},
|
||||
): string {
|
||||
let locale: Locale | null = null;
|
||||
if (this.messages[this._currentLocale][id]) {
|
||||
locale = this._currentLocale;
|
||||
} else if (this.messages[this.defaultLocale][id]) {
|
||||
locale = this.defaultLocale;
|
||||
}
|
||||
const str = this.messages[this._currentLocale][id]
|
||||
|| this.messages[this.defaultLocale][id]
|
||||
|| id;
|
||||
|
||||
if (!locale) {
|
||||
throw new Error(`No translation for id: ${id}, neither in current locale: ${this._currentLocale} nor defaulkt locale ${this.defaultLocale}`);
|
||||
}
|
||||
|
||||
return this._formatter(translator<Formater>(
|
||||
getForm(str, number, locale as AvailableLocales, id),
|
||||
{ count: number, ...params },
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
export default Translator;
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from './Translator';
|
||||
export { SupportedLangs, GenericLocales, AvailableLocales, checkFormsExternal as checkForms } from './lib/plural';
|
||||
@@ -1,100 +0,0 @@
|
||||
import {
|
||||
isTextNode,
|
||||
isTagNode,
|
||||
isPlaceholderNode,
|
||||
isVoidTagNode,
|
||||
NODE,
|
||||
} from './nodes';
|
||||
|
||||
/**
|
||||
* Checks if target is function
|
||||
* @param target
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isFunction = (target: any) => {
|
||||
return typeof target === 'function';
|
||||
};
|
||||
|
||||
type FormatingFunc<T = string> = (chunks: string) => T;
|
||||
export type AllowedValues<T> = Record<string, number | string | FormatingFunc<T>>;
|
||||
|
||||
/**
|
||||
* This function accepts an AST (abstract syntax tree) which is a result
|
||||
* of the parser function call, and converts tree nodes into array of strings replacing node
|
||||
* values with provided values.
|
||||
* Values is a map with functions or strings, where each key is related to placeholder value
|
||||
* or tag value
|
||||
* e.g.
|
||||
* string "text <tag>tag text</tag> %placeholder%" is parsed into next AST
|
||||
*
|
||||
* [
|
||||
* { type: 'text', value: 'text ' },
|
||||
* {
|
||||
* type: 'tag',
|
||||
* value: 'tag',
|
||||
* children: [{ type: 'text', value: 'tag text' }],
|
||||
* },
|
||||
* { type: 'text', value: ' ' },
|
||||
* { type: 'placeholder', value: 'placeholder' }
|
||||
* ];
|
||||
*
|
||||
* this AST after format and next values
|
||||
*
|
||||
* {
|
||||
* // here used template strings, but it can be react components as well
|
||||
* tag: (chunks) => `<b>${chunks}</b>`,
|
||||
* placeholder: 'placeholder text'
|
||||
* }
|
||||
*
|
||||
* will return next array
|
||||
*
|
||||
* [ 'text ', '<b>tag text</b>', ' ', 'placeholder text' ]
|
||||
*
|
||||
* as you can see, <tag> was replaced by <b>, and placeholder was replaced by placeholder text
|
||||
*
|
||||
* @param ast - AST (abstract syntax tree)
|
||||
* @param values
|
||||
* @returns {[]}
|
||||
*/
|
||||
const format = <T = any | string>(ast: NODE[], values: AllowedValues<T>) => {
|
||||
const result: (string | T)[] = [];
|
||||
let i = 0;
|
||||
while (i < ast.length) {
|
||||
const currentNode = ast[i];
|
||||
// if current node is text node, there is nothing to change, append value to the result
|
||||
if (isTextNode(currentNode)) {
|
||||
result.push(currentNode.value);
|
||||
} else if (isTagNode(currentNode)) {
|
||||
const children = [...format(currentNode.children ? currentNode.children : [], values)].join('');
|
||||
const value = values[currentNode.value];
|
||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'function') {
|
||||
if (isFunction(value)) {
|
||||
result.push((value as FormatingFunc<T>)(children));
|
||||
} else {
|
||||
result.push(value.toString());
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Value ${currentNode.value} wasn't provided`);
|
||||
}
|
||||
} else if (isVoidTagNode(currentNode)) {
|
||||
const value = values[currentNode.value];
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
result.push(value.toString());
|
||||
} else {
|
||||
throw new Error(`Value ${currentNode.value} wasn't provided`);
|
||||
}
|
||||
} else if (isPlaceholderNode(currentNode)) {
|
||||
const value = values[currentNode.value];
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
result.push(value.toString());
|
||||
} else {
|
||||
throw new Error(`Value ${currentNode.value} wasn't provided`);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default format;
|
||||
@@ -1,50 +0,0 @@
|
||||
export enum NODE_TYPES {
|
||||
PLACEHOLDER = 'placeholder',
|
||||
TEXT = 'text',
|
||||
TAG = 'tag',
|
||||
VOID_TAG = 'void_tag',
|
||||
}
|
||||
|
||||
export interface NODE {
|
||||
type: NODE_TYPES;
|
||||
value: string | keyof HTMLElementTagNameMap;
|
||||
children?: NODE[];
|
||||
}
|
||||
|
||||
export const isTextNode = (node: NODE) => {
|
||||
return node?.type === NODE_TYPES.TEXT;
|
||||
};
|
||||
|
||||
export const isTagNode = (node: NODE) => {
|
||||
return node?.type === NODE_TYPES.TAG;
|
||||
};
|
||||
|
||||
export const isPlaceholderNode = (node: NODE) => {
|
||||
return node?.type === NODE_TYPES.PLACEHOLDER;
|
||||
};
|
||||
|
||||
export const isVoidTagNode = (node: NODE) => {
|
||||
return node?.type === NODE_TYPES.VOID_TAG;
|
||||
};
|
||||
|
||||
export const placeholderNode = (value: string) => {
|
||||
return { type: NODE_TYPES.PLACEHOLDER, value };
|
||||
};
|
||||
|
||||
export const textNode = (str: string) => {
|
||||
return { type: NODE_TYPES.TEXT, value: str };
|
||||
};
|
||||
|
||||
export const tagNode = (tagName: keyof HTMLElementTagNameMap, children: NODE[]) => {
|
||||
const value = tagName.trim();
|
||||
return { type: NODE_TYPES.TAG, value, children };
|
||||
};
|
||||
|
||||
export const voidTagNode = (tagName: keyof HTMLElementTagNameMap) => {
|
||||
const value = tagName.trim();
|
||||
return { type: NODE_TYPES.VOID_TAG, value };
|
||||
};
|
||||
|
||||
export const isNode = (checked: any) => {
|
||||
return !!checked?.type;
|
||||
};
|
||||
@@ -1,335 +0,0 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import {
|
||||
tagNode,
|
||||
textNode,
|
||||
isNode,
|
||||
placeholderNode,
|
||||
voidTagNode,
|
||||
NODE,
|
||||
} from './nodes';
|
||||
|
||||
enum STATE {
|
||||
/**
|
||||
* parser function switches to the text state when parses simple text,
|
||||
* or content between open and close tags
|
||||
*/
|
||||
TEXT = 'text',
|
||||
|
||||
/**
|
||||
* parser function switches to the tag state when meets open tag brace ("<"), and switches back,
|
||||
* when meets closing tag brace (">")
|
||||
*/
|
||||
TAG = 'tag',
|
||||
|
||||
/**
|
||||
* Parser function switches to the placeholder state when meets in the text
|
||||
* open placeholders brace ("{") and switches back to the text state,
|
||||
* when meets close placeholder brace ("}")
|
||||
*/
|
||||
PLACEHOLDER = 'placeholder',
|
||||
}
|
||||
|
||||
enum CONTROL_CHARS {
|
||||
TAG_OPEN_BRACE = '<',
|
||||
TAG_CLOSE_BRACE = '>',
|
||||
CLOSING_TAG_MARK = '/',
|
||||
PLACEHOLDER_MARK = '%',
|
||||
}
|
||||
|
||||
interface Context {
|
||||
/**
|
||||
* Stack is used to keep and search nested tag nodes
|
||||
* @type {*[]}
|
||||
*/
|
||||
stack: (NODE | keyof HTMLElementTagNameMap)[];
|
||||
/**
|
||||
* Result is stack where function allocates nodes
|
||||
* @type {*[]}
|
||||
*/
|
||||
result: NODE[];
|
||||
/**
|
||||
* Current char index
|
||||
* @type {number}
|
||||
*/
|
||||
currIdx: number;
|
||||
/**
|
||||
* Saves index of the last state change from the text state,
|
||||
* used to restore parsed text if we moved into other state wrongly
|
||||
*/
|
||||
lastTextStateChangeIdx: number;
|
||||
/**
|
||||
* Accumulated tag value
|
||||
*/
|
||||
tag: string;
|
||||
/**
|
||||
* Accumulated text value
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
* Accumulated placeholder value
|
||||
*/
|
||||
placeholder: string;
|
||||
/**
|
||||
* Parsed string
|
||||
*/
|
||||
str: string;
|
||||
/**
|
||||
* Currently parsed char
|
||||
*/
|
||||
currChar: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text length is enough to create text node
|
||||
* If text node created, then if stack is not empty it is pushed into stack,
|
||||
* otherwise into result
|
||||
* @param context
|
||||
*/
|
||||
const createTextNodeIfPossible = (context: Context) => {
|
||||
const { text } = context;
|
||||
|
||||
if (text.length > 0) {
|
||||
const node = textNode(text);
|
||||
if (context.stack.length > 0) {
|
||||
context.stack.push(node);
|
||||
} else {
|
||||
context.result.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
context.text = '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles text state
|
||||
* @returns {function}
|
||||
*/
|
||||
const textStateHandler = (context: Context) => {
|
||||
const { currChar, currIdx } = context;
|
||||
|
||||
// switches to the tag state
|
||||
if (currChar === CONTROL_CHARS.TAG_OPEN_BRACE) {
|
||||
context.lastTextStateChangeIdx = currIdx;
|
||||
return STATE.TAG;
|
||||
}
|
||||
|
||||
// switches to the placeholder state
|
||||
if (currChar === CONTROL_CHARS.PLACEHOLDER_MARK) {
|
||||
context.lastTextStateChangeIdx = currIdx;
|
||||
return STATE.PLACEHOLDER;
|
||||
}
|
||||
|
||||
// remains in the text state
|
||||
context.text += currChar;
|
||||
return STATE.TEXT;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles placeholder state
|
||||
* @param context
|
||||
* @returns {string}
|
||||
*/
|
||||
const placeholderStateHandler = (context: Context) => {
|
||||
const {
|
||||
currChar,
|
||||
currIdx,
|
||||
lastTextStateChangeIdx,
|
||||
placeholder,
|
||||
stack,
|
||||
result,
|
||||
str,
|
||||
} = context;
|
||||
|
||||
if (currChar === CONTROL_CHARS.PLACEHOLDER_MARK) {
|
||||
// if distance between current index and last state change equal to 1,
|
||||
// it means that placeholder mark was escaped by itself e.g. "%%",
|
||||
// so we return to the text state
|
||||
if (currIdx - lastTextStateChangeIdx === 1) {
|
||||
context.text += str.substring(lastTextStateChangeIdx, currIdx);
|
||||
return STATE.TEXT;
|
||||
}
|
||||
|
||||
createTextNodeIfPossible(context);
|
||||
const node = placeholderNode(placeholder);
|
||||
|
||||
// push node to the appropriate stack
|
||||
if (stack.length > 0) {
|
||||
stack.push(node);
|
||||
} else {
|
||||
result.push(node);
|
||||
}
|
||||
|
||||
context.placeholder = '';
|
||||
return STATE.TEXT;
|
||||
}
|
||||
|
||||
context.placeholder += currChar;
|
||||
return STATE.PLACEHOLDER;
|
||||
};
|
||||
|
||||
/**
|
||||
* Switches current state to the tag state and returns tag state handler
|
||||
* @returns {function}
|
||||
*/
|
||||
const tagStateHandler = (context: Context) => {
|
||||
const {
|
||||
currChar,
|
||||
text,
|
||||
stack,
|
||||
result,
|
||||
lastTextStateChangeIdx,
|
||||
currIdx,
|
||||
str,
|
||||
} = context;
|
||||
|
||||
let { tag } = context;
|
||||
|
||||
// if found tag end ">"
|
||||
if (currChar === CONTROL_CHARS.TAG_CLOSE_BRACE) {
|
||||
// if the tag is close tag e.g. </a>
|
||||
if (tag.indexOf(CONTROL_CHARS.CLOSING_TAG_MARK) === 0) {
|
||||
// remove slash from tag
|
||||
tag = tag.substring(1);
|
||||
|
||||
let children: NODE[] = [];
|
||||
if (text.length > 0) {
|
||||
children.push(textNode(text));
|
||||
context.text = '';
|
||||
}
|
||||
|
||||
let pairTagFound = false;
|
||||
// looking for the pair to the close tag
|
||||
while (!pairTagFound && stack.length > 0) {
|
||||
const lastFromStack = stack.pop();
|
||||
// if tag from stack equal to close tag
|
||||
if (lastFromStack === tag) {
|
||||
// create tag node
|
||||
const node = tagNode(tag as keyof HTMLElementTagNameMap, children);
|
||||
// and add it to the appropriate stack
|
||||
if (stack.length > 0) {
|
||||
stack.push(node);
|
||||
} else {
|
||||
result.push(node);
|
||||
}
|
||||
children = [];
|
||||
pairTagFound = true;
|
||||
} else if (isNode(lastFromStack)) {
|
||||
// add nodes between close tag and open tag to the children
|
||||
children.unshift(lastFromStack as NODE);
|
||||
} else {
|
||||
throw new Error(`String has unbalanced tags: ${str}`);
|
||||
}
|
||||
if (stack.length === 0 && children.length > 0) {
|
||||
throw new Error(`String has unbalanced tags: ${str}`);
|
||||
}
|
||||
}
|
||||
context.tag = '';
|
||||
return STATE.TEXT;
|
||||
}
|
||||
|
||||
// if the tag is void tag e.g. <img/>
|
||||
if (tag.lastIndexOf(CONTROL_CHARS.CLOSING_TAG_MARK) === tag.length - 1) {
|
||||
tag = tag.substring(0, tag.length - 1);
|
||||
createTextNodeIfPossible(context);
|
||||
const node = voidTagNode(tag as keyof HTMLElementTagNameMap);
|
||||
// add node to the appropriate stack
|
||||
if (stack.length > 0) {
|
||||
stack.push(node);
|
||||
} else {
|
||||
result.push(node);
|
||||
}
|
||||
context.tag = '';
|
||||
return STATE.TEXT;
|
||||
}
|
||||
|
||||
createTextNodeIfPossible(context);
|
||||
stack.push(tag as keyof HTMLElementTagNameMap);
|
||||
context.tag = '';
|
||||
return STATE.TEXT;
|
||||
}
|
||||
|
||||
// If we meet open tag "<" it means that we wrongly moved into tag state
|
||||
if (currChar === CONTROL_CHARS.TAG_OPEN_BRACE) {
|
||||
context.text += str.substring(lastTextStateChangeIdx, currIdx);
|
||||
context.lastTextStateChangeIdx = currIdx;
|
||||
context.tag = '';
|
||||
return STATE.TAG;
|
||||
}
|
||||
|
||||
context.tag += currChar;
|
||||
return STATE.TAG;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses string into AST (abstract syntax tree) and returns it
|
||||
* e.g.
|
||||
* parse("String to <a>translate</a>") ->
|
||||
* ```
|
||||
* [
|
||||
* { type: 'text', value: 'String to ' },
|
||||
* { type: 'tag', value: 'a', children: [{ type: 'text', value: 'translate' }] }
|
||||
* ];
|
||||
* ```
|
||||
* Empty string is parsed into empty AST (abstract syntax tree): "[]"
|
||||
* If founds unbalanced tags, it throws error about it
|
||||
*
|
||||
* @param {string} str - message in simplified ICU like syntax without plural support
|
||||
* @returns {[]}
|
||||
*/
|
||||
const parser = (str = '') => {
|
||||
const context: Context = {
|
||||
str,
|
||||
stack: [],
|
||||
result: [],
|
||||
currIdx: 0,
|
||||
lastTextStateChangeIdx: 0,
|
||||
tag: '',
|
||||
text: '',
|
||||
placeholder: '',
|
||||
currChar: '',
|
||||
};
|
||||
|
||||
const STATE_HANDLERS = {
|
||||
[STATE.TEXT]: textStateHandler,
|
||||
[STATE.PLACEHOLDER]: placeholderStateHandler,
|
||||
[STATE.TAG]: tagStateHandler,
|
||||
};
|
||||
|
||||
// Start from text state
|
||||
let currentState = STATE.TEXT;
|
||||
|
||||
while (context.currIdx < str.length) {
|
||||
context.currChar = str[context.currIdx];
|
||||
const currentStateHandler: (c: Context) => STATE = STATE_HANDLERS[currentState];
|
||||
currentState = currentStateHandler(context);
|
||||
context.currIdx += 1;
|
||||
}
|
||||
|
||||
const {
|
||||
result,
|
||||
text,
|
||||
stack,
|
||||
lastTextStateChangeIdx,
|
||||
} = context;
|
||||
|
||||
// Means that tag or placeholder nodes were not closed, so we consider them as text
|
||||
if (currentState !== STATE.TEXT) {
|
||||
const restText = str.substring(lastTextStateChangeIdx);
|
||||
if ((restText + text).length > 0) {
|
||||
result.push(textNode(text + restText));
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (text.length > 0) {
|
||||
result.push(textNode(text));
|
||||
}
|
||||
}
|
||||
|
||||
if (stack.length > 0) {
|
||||
throw new Error(`String has unbalanced tags ${context.str}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default parser;
|
||||
@@ -1,394 +0,0 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
export type SupportedLangs = 'az' | 'bo' | 'dz' | 'id' | 'ja' | 'jv' | 'ka' | 'km' | 'kn' | 'ko' | 'ms' | 'th' | 'tr' | 'vi' | 'zh' | 'af' | 'bn' | 'bg' | 'ca' | 'da' | 'de' | 'el' | 'en' | 'eo' | 'es' | 'et' | 'eu' | 'fa' | 'fi' | 'fo' | 'fur' | 'fy' | 'gl' | 'gu' | 'ha' | 'he' | 'hu' | 'is' | 'it' | 'ku' | 'lb' | 'ml' | 'mn' | 'mr' | 'nah' | 'nb' | 'ne' | 'nl' | 'nn' | 'no' | 'oc' | 'om' | 'or' | 'pa' | 'pap' | 'ps' | 'pt' | 'so' | 'sq' | 'sv' | 'sw' | 'ta' | 'te' | 'tk' | 'ur' | 'zu' | 'am' | 'bh' | 'fil' | 'fr' | 'gun' | 'hi' | 'hy' | 'ln' | 'mg' | 'nso' | 'xbr' | 'ti' | 'wa' | 'be' | 'bs' | 'hr' | 'ru' | 'sr' | 'uk' | 'cs' | 'sk' | 'ga' | 'lt' | 'sl' | 'mk' | 'mt' | 'lv' | 'pl' | 'cy' | 'ro' | 'ar';
|
||||
|
||||
export type GenericLocales = {
|
||||
[key in SupportedLangs]: SupportedLangs;
|
||||
};
|
||||
|
||||
export enum AvailableLocales {
|
||||
az = 'az',
|
||||
bo = 'bo',
|
||||
dz = 'dz',
|
||||
id = 'id',
|
||||
ja = 'ja',
|
||||
jv = 'jv',
|
||||
ka = 'ka',
|
||||
km = 'km',
|
||||
kn = 'kn',
|
||||
ko = 'ko',
|
||||
ms = 'ms',
|
||||
th = 'th',
|
||||
tr = 'tr',
|
||||
vi = 'vi',
|
||||
zh = 'zh',
|
||||
af = 'af',
|
||||
bn = 'bn',
|
||||
bg = 'bg',
|
||||
ca = 'ca',
|
||||
da = 'da',
|
||||
de = 'de',
|
||||
el = 'el',
|
||||
en = 'en',
|
||||
eo = 'eo',
|
||||
es = 'es',
|
||||
et = 'et',
|
||||
eu = 'eu',
|
||||
fa = 'fa',
|
||||
fi = 'fi',
|
||||
fo = 'fo',
|
||||
fur = 'fur',
|
||||
fy = 'fy',
|
||||
gl = 'gl',
|
||||
gu = 'gu',
|
||||
ha = 'ha',
|
||||
he = 'he',
|
||||
hu = 'hu',
|
||||
is = 'is',
|
||||
it = 'it',
|
||||
ku = 'ku',
|
||||
lb = 'lb',
|
||||
ml = 'ml',
|
||||
mn = 'mn',
|
||||
mr = 'mr',
|
||||
nah = 'nah',
|
||||
nb = 'nb',
|
||||
ne = 'ne',
|
||||
nl = 'nl',
|
||||
nn = 'nn',
|
||||
no = 'no',
|
||||
oc = 'oc',
|
||||
om = 'om',
|
||||
or = 'or',
|
||||
pa = 'pa',
|
||||
pap = 'pap',
|
||||
ps = 'ps',
|
||||
pt = 'pt',
|
||||
so = 'so',
|
||||
sq = 'sq',
|
||||
sv = 'sv',
|
||||
sw = 'sw',
|
||||
ta = 'ta',
|
||||
te = 'te',
|
||||
tk = 'tk',
|
||||
ur = 'ur',
|
||||
zu = 'zu',
|
||||
am = 'am',
|
||||
bh = 'bh',
|
||||
fil = 'fil',
|
||||
fr = 'fr',
|
||||
gun = 'gun',
|
||||
hi = 'hi',
|
||||
hy = 'hy',
|
||||
ln = 'ln',
|
||||
mg = 'mg',
|
||||
nso = 'nso',
|
||||
xbr = 'xbr',
|
||||
ti = 'ti',
|
||||
wa = 'wa',
|
||||
be = 'be',
|
||||
bs = 'bs',
|
||||
hr = 'hr',
|
||||
ru = 'ru',
|
||||
sr = 'sr',
|
||||
uk = 'uk',
|
||||
cs = 'cs',
|
||||
sk = 'sk',
|
||||
ga = 'ga',
|
||||
lt = 'lt',
|
||||
sl = 'sl',
|
||||
mk = 'mk',
|
||||
mt = 'mt',
|
||||
lv = 'lv',
|
||||
pl = 'pl',
|
||||
cy = 'cy',
|
||||
ro = 'ro',
|
||||
ar = 'ar',
|
||||
}
|
||||
export const getPluralFormId = (locale: AvailableLocales, number: number) => {
|
||||
if (number === 0) {
|
||||
return 0;
|
||||
}
|
||||
const slavNum = ((number % 10 === 1) && (number % 100 !== 11))
|
||||
? 1
|
||||
: (
|
||||
((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10)
|
||||
|| (number % 100 >= 20))
|
||||
)
|
||||
? 2
|
||||
: 3);
|
||||
const supportedForms: Record<AvailableLocales, number> = {
|
||||
[AvailableLocales.az]: 1,
|
||||
[AvailableLocales.bo]: 1,
|
||||
[AvailableLocales.dz]: 1,
|
||||
[AvailableLocales.id]: 1,
|
||||
[AvailableLocales.ja]: 1,
|
||||
[AvailableLocales.jv]: 1,
|
||||
[AvailableLocales.ka]: 1,
|
||||
[AvailableLocales.km]: 1,
|
||||
[AvailableLocales.kn]: 1,
|
||||
[AvailableLocales.ko]: 1,
|
||||
[AvailableLocales.ms]: 1,
|
||||
[AvailableLocales.th]: 1,
|
||||
[AvailableLocales.tr]: 1,
|
||||
[AvailableLocales.vi]: 1,
|
||||
[AvailableLocales.zh]: 1,
|
||||
|
||||
[AvailableLocales.af]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.bn]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.bg]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.ca]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.da]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.de]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.el]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.en]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.eo]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.es]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.et]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.eu]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.fa]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.fi]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.fo]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.fur]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.fy]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.gl]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.gu]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.ha]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.he]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.hu]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.is]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.it]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.ku]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.lb]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.ml]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.mn]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.mr]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.nah]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.nb]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.ne]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.nl]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.nn]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.no]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.oc]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.om]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.or]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.pa]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.pap]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.ps]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.pt]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.so]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.sq]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.sv]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.sw]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.ta]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.te]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.tk]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.ur]: (number === 1) ? 1 : 2,
|
||||
[AvailableLocales.zu]: (number === 1) ? 1 : 2,
|
||||
|
||||
// how it works with 0?
|
||||
[AvailableLocales.am]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.bh]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.fil]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.fr]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.gun]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.hi]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.hy]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.ln]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.mg]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.nso]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.xbr]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.ti]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
[AvailableLocales.wa]: ((number === 0) || (number === 1)) ? 0 : 1,
|
||||
|
||||
[AvailableLocales.be]: slavNum,
|
||||
[AvailableLocales.bs]: slavNum,
|
||||
[AvailableLocales.hr]: slavNum,
|
||||
[AvailableLocales.ru]: slavNum,
|
||||
[AvailableLocales.sr]: slavNum,
|
||||
[AvailableLocales.uk]: slavNum,
|
||||
|
||||
[AvailableLocales.cs]: (number === 1) ? 1 : (((number >= 2) && (number <= 4)) ? 2 : 3),
|
||||
[AvailableLocales.sk]: (number === 1) ? 1 : (((number >= 2) && (number <= 4)) ? 2 : 3),
|
||||
[AvailableLocales.ga]: (number === 1) ? 1 : ((number === 2) ? 2 : 3),
|
||||
[AvailableLocales.lt]: ((number % 10 === 1) && (number % 100 !== 11))
|
||||
? 1
|
||||
: (((number % 10 >= 2) && ((number % 100 < 10) || (number % 100 >= 20)))
|
||||
? 2
|
||||
: 3),
|
||||
[AvailableLocales.sl]: (number % 100 === 1)
|
||||
? 1
|
||||
: ((number % 100 === 2)
|
||||
? 2
|
||||
: (((number % 100 === 3) || (number % 100 === 4))
|
||||
? 3
|
||||
: 4)),
|
||||
[AvailableLocales.mk]: (number % 10 === 1) ? 1 : 2,
|
||||
[AvailableLocales.mt]: (number === 1)
|
||||
? 1
|
||||
: (((number === 0) || ((number % 100 > 1) && (number % 100 < 11)))
|
||||
? 2
|
||||
: (((number % 100 > 10) && (number % 100 < 20))
|
||||
? 3
|
||||
: 4)),
|
||||
[AvailableLocales.lv]: (number === 0)
|
||||
? 0
|
||||
: (((number % 10 === 1) && (number % 100 !== 11))
|
||||
? 1
|
||||
: 2),
|
||||
[AvailableLocales.pl]: (number === 1)
|
||||
? 1
|
||||
: (
|
||||
((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 12)
|
||||
|| (number % 100 > 14))
|
||||
)
|
||||
? 2
|
||||
: 3),
|
||||
[AvailableLocales.cy]: (number === 1)
|
||||
? 0
|
||||
: ((number === 2)
|
||||
? 1
|
||||
: (((number === 8) || (number === 11))
|
||||
? 2
|
||||
: 3)),
|
||||
[AvailableLocales.ro]: (number === 1)
|
||||
? 1
|
||||
: (((number === 1) || ((number % 100 > 0) && (number % 100 < 20)))
|
||||
? 2
|
||||
: 3),
|
||||
[AvailableLocales.ar]: (number === 0)
|
||||
? 0
|
||||
: ((number === 1)
|
||||
? 1
|
||||
: ((number === 2)
|
||||
? 2
|
||||
: (((number % 100 >= 3) && (number % 100 <= 10))
|
||||
? 3
|
||||
: (((number % 100 >= 11) && (number % 100 <= 99))
|
||||
? 4
|
||||
: 5)))),
|
||||
|
||||
};
|
||||
return supportedForms[locale];
|
||||
};
|
||||
export const pluraFormsCount: Record<AvailableLocales, number> = {
|
||||
[AvailableLocales.az]: 2,
|
||||
[AvailableLocales.bo]: 2,
|
||||
[AvailableLocales.dz]: 2,
|
||||
[AvailableLocales.id]: 2,
|
||||
[AvailableLocales.ja]: 2,
|
||||
[AvailableLocales.jv]: 2,
|
||||
[AvailableLocales.ka]: 2,
|
||||
[AvailableLocales.km]: 2,
|
||||
[AvailableLocales.kn]: 2,
|
||||
[AvailableLocales.ko]: 2,
|
||||
[AvailableLocales.ms]: 2,
|
||||
[AvailableLocales.th]: 2,
|
||||
[AvailableLocales.tr]: 2,
|
||||
[AvailableLocales.vi]: 2,
|
||||
[AvailableLocales.zh]: 2,
|
||||
[AvailableLocales.af]: 3,
|
||||
[AvailableLocales.bn]: 3,
|
||||
[AvailableLocales.bg]: 3,
|
||||
[AvailableLocales.ca]: 3,
|
||||
[AvailableLocales.da]: 3,
|
||||
[AvailableLocales.de]: 3,
|
||||
[AvailableLocales.el]: 3,
|
||||
[AvailableLocales.en]: 3,
|
||||
[AvailableLocales.eo]: 3,
|
||||
[AvailableLocales.es]: 3,
|
||||
[AvailableLocales.et]: 3,
|
||||
[AvailableLocales.eu]: 3,
|
||||
[AvailableLocales.fa]: 3,
|
||||
[AvailableLocales.fi]: 3,
|
||||
[AvailableLocales.fo]: 3,
|
||||
[AvailableLocales.fur]: 3,
|
||||
[AvailableLocales.fy]: 3,
|
||||
[AvailableLocales.gl]: 3,
|
||||
[AvailableLocales.gu]: 3,
|
||||
[AvailableLocales.ha]: 3,
|
||||
[AvailableLocales.he]: 3,
|
||||
[AvailableLocales.hu]: 3,
|
||||
[AvailableLocales.is]: 3,
|
||||
[AvailableLocales.it]: 3,
|
||||
[AvailableLocales.ku]: 3,
|
||||
[AvailableLocales.lb]: 3,
|
||||
[AvailableLocales.ml]: 3,
|
||||
[AvailableLocales.mn]: 3,
|
||||
[AvailableLocales.mr]: 3,
|
||||
[AvailableLocales.nah]: 3,
|
||||
[AvailableLocales.nb]: 3,
|
||||
[AvailableLocales.ne]: 3,
|
||||
[AvailableLocales.nl]: 3,
|
||||
[AvailableLocales.nn]: 3,
|
||||
[AvailableLocales.no]: 3,
|
||||
[AvailableLocales.oc]: 3,
|
||||
[AvailableLocales.om]: 3,
|
||||
[AvailableLocales.or]: 3,
|
||||
[AvailableLocales.pa]: 3,
|
||||
[AvailableLocales.pap]: 3,
|
||||
[AvailableLocales.ps]: 3,
|
||||
[AvailableLocales.pt]: 3,
|
||||
[AvailableLocales.so]: 3,
|
||||
[AvailableLocales.sq]: 3,
|
||||
[AvailableLocales.sv]: 3,
|
||||
[AvailableLocales.sw]: 3,
|
||||
[AvailableLocales.ta]: 3,
|
||||
[AvailableLocales.te]: 3,
|
||||
[AvailableLocales.tk]: 3,
|
||||
[AvailableLocales.ur]: 3,
|
||||
[AvailableLocales.zu]: 3,
|
||||
[AvailableLocales.am]: 2,
|
||||
[AvailableLocales.bh]: 2,
|
||||
[AvailableLocales.fil]: 2,
|
||||
[AvailableLocales.fr]: 2,
|
||||
[AvailableLocales.gun]: 2,
|
||||
[AvailableLocales.hi]: 2,
|
||||
[AvailableLocales.hy]: 2,
|
||||
[AvailableLocales.ln]: 2,
|
||||
[AvailableLocales.mg]: 2,
|
||||
[AvailableLocales.nso]: 2,
|
||||
[AvailableLocales.xbr]: 2,
|
||||
[AvailableLocales.ti]: 2,
|
||||
[AvailableLocales.wa]: 2,
|
||||
[AvailableLocales.be]: 4,
|
||||
[AvailableLocales.bs]: 4,
|
||||
[AvailableLocales.hr]: 4,
|
||||
[AvailableLocales.ru]: 4,
|
||||
[AvailableLocales.sr]: 4,
|
||||
[AvailableLocales.uk]: 4,
|
||||
[AvailableLocales.cs]: 4,
|
||||
[AvailableLocales.sk]: 4,
|
||||
[AvailableLocales.ga]: 4,
|
||||
[AvailableLocales.lt]: 4,
|
||||
[AvailableLocales.sl]: 5,
|
||||
[AvailableLocales.mk]: 3,
|
||||
[AvailableLocales.mt]: 5,
|
||||
[AvailableLocales.lv]: 3,
|
||||
[AvailableLocales.pl]: 4,
|
||||
[AvailableLocales.cy]: 4,
|
||||
[AvailableLocales.ro]: 4,
|
||||
[AvailableLocales.ar]: 6,
|
||||
};
|
||||
|
||||
const PLURAL_STRING_DELIMITER = '|';
|
||||
|
||||
export const checkForms = (str: string, locale: AvailableLocales, id: string) => {
|
||||
const forms = str.split(PLURAL_STRING_DELIMITER);
|
||||
if (forms.length !== pluraFormsCount[locale]) {
|
||||
throw new Error(`Invalid plural string "${id}" for locale ${locale}: ${forms.length} given; need: ${pluraFormsCount[locale]}`);
|
||||
}
|
||||
};
|
||||
export const checkFormsExternal = (str: string, locale: AvailableLocales, id: string) => {
|
||||
try {
|
||||
checkForms(str, locale, id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export const getForm = (str: string, number: number, locale: AvailableLocales, id: string) => {
|
||||
checkForms(str, locale, id);
|
||||
const forms = str.split(PLURAL_STRING_DELIMITER);
|
||||
const currentForm = getPluralFormId(locale, number);
|
||||
return forms[currentForm].trim();
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import parser from './parser';
|
||||
import format, { AllowedValues } from './formatter';
|
||||
|
||||
const translator = <T>(message: string, values: AllowedValues<T>) => {
|
||||
const astMessage = parser(message);
|
||||
const formatted = format<T>(astMessage, values);
|
||||
return formatted;
|
||||
};
|
||||
export default translator;
|
||||
@@ -1,60 +0,0 @@
|
||||
import parser from './parser';
|
||||
import { isTextNode, NODE } from './nodes';
|
||||
|
||||
/**
|
||||
* Compares two AST (abstract syntax tree) structures,
|
||||
* view tests for examples
|
||||
* @param baseAst
|
||||
* @param targetAst
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const areAstStructuresSame = (baseAst: NODE[], targetAst: NODE[]) => {
|
||||
const textNodeFilter = (node: NODE) => {
|
||||
return !isTextNode(node);
|
||||
};
|
||||
|
||||
const filteredBaseAst = baseAst.filter(textNodeFilter);
|
||||
|
||||
const filteredTargetAst = targetAst.filter(textNodeFilter);
|
||||
|
||||
// if AST structures have different lengths, they are not equal
|
||||
if (filteredBaseAst.length !== filteredTargetAst.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < filteredBaseAst.length; i += 1) {
|
||||
const baseNode = filteredBaseAst[i];
|
||||
|
||||
const targetNode = filteredTargetAst.find((node) => {
|
||||
return node.type === baseNode.type && node.value === baseNode.value;
|
||||
});
|
||||
|
||||
if (!targetNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetNode.children && baseNode.children) {
|
||||
const areChildrenSame = areAstStructuresSame(baseNode.children, targetNode.children);
|
||||
if (!areChildrenSame) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates translation against base string by AST (abstract syntax tree) structure
|
||||
* @param baseStr
|
||||
* @param targetStr
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isTargetStrValid = (baseStr: string, targetStr: string) => {
|
||||
const baseAst = parser(baseStr);
|
||||
const targetAst = parser(targetStr);
|
||||
|
||||
const result = areAstStructuresSame(baseAst, targetAst);
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
import T from './Translator';
|
||||
import { Locale } from './locales';
|
||||
import { Locale, DatePickerLocale, messages, DEFAULT_LOCALE, LANGUAGES } from './locales';
|
||||
|
||||
export { messages, DatePickerLocale, Locale, DEFAULT_LOCALE, LANGUAGES, reactFormater } from './locales';
|
||||
export type Translator = T<Locale>;
|
||||
export default T;
|
||||
export { Locale, DatePickerLocale, messages, DEFAULT_LOCALE, LANGUAGES };
|
||||
export const i18n = (lang: Locale) => ({
|
||||
getMessage: (key: string) => messages[lang][key],
|
||||
getUILanguage: () => lang,
|
||||
getBaseMessage: (key: string) => messages[DEFAULT_LOCALE][key] || key,
|
||||
getBaseUILanguage: () => DEFAULT_LOCALE,
|
||||
});
|
||||
|
||||
@@ -7,6 +7,34 @@
|
||||
"next": "Next",
|
||||
"port": "Port",
|
||||
"router": "Router",
|
||||
"username": "Username",
|
||||
"sign_in": "Sign in",
|
||||
"sign_out": "Sign out",
|
||||
"dashboard": "Dashboard",
|
||||
"setup_guide": "Setup guide",
|
||||
"query_log": "Query Log",
|
||||
"filters": "Filters",
|
||||
"settings": "Settings",
|
||||
"general_settings": "General settings",
|
||||
"dns_settings": "DNS settings",
|
||||
"encryption_settings": "Encryption settings",
|
||||
"client_settings": "Client settings",
|
||||
"dhcp_settings": "DHCP settings",
|
||||
"disable": "Disable",
|
||||
"disabled": "Disabled",
|
||||
"enable": "Enable",
|
||||
"clear": "Clear",
|
||||
"cancel": "Cancel",
|
||||
|
||||
"login_password_title": "Reset Password",
|
||||
"login_password_link": "Forgot password?",
|
||||
"login_password_hash": "AdGuard Home stores passwords as a BCrypt-encoded hash. Here's what you need to do to change the password:",
|
||||
"login_password_step_1": "Stop AdGuard Home",
|
||||
"login_password_step_2": "Edit <code>AdGuardHome.yaml</code>",
|
||||
"login_password_step_3": "Find <code>password</code> field there",
|
||||
"login_password_step_4": "Replace it with the new value. You can use .htpasswd password generator tool or any online BCrypt generator tool (there are many available online).",
|
||||
"login_password_step_5": "Start AdGuard Home",
|
||||
"login_password_result": "Now you'll be able to log in to web interface using your new password.",
|
||||
|
||||
"install_admin_interface_port_desc": "Now it is working at 3000 port, just in case, but we recomended to use 80 port. Using this ports allow to access to Web interface like to common site",
|
||||
"install_admin_interface_port": "Which port will be used",
|
||||
@@ -44,5 +72,66 @@
|
||||
"install_configure_android": "<p>From the Android Menu home screen, tap Settings.</p><p>Tap Wi-Fi on the menu. The screen listing all of the available networks will be shown (it is impossible to set custom DNS for mobile connection).</p><p>Long press the network you're connected to, and tap Modify Network.</p><p>On some devices, you may need to check the box for Advanced to see further settings. To adjust your Android DNS settings, you will need to switch the IP settings from DHCP to Static.</p><p>Change set DNS 1 and DNS 2 values to your AdGuard Home server addresses.</p>",
|
||||
"install_configure_ios": "<p>From the home screen, tap Settings.</p><p>Choose Wi-Fi in the left menu (it is impossible to configure DNS for mobile networks).</p><p>Tap on the name of the currently active network.</p><p>In the DNS field enter your AdGuard Home server addresses.</p>",
|
||||
"install_configure_adresses": "AdGuard Home addresses:",
|
||||
"install_configure_dhcp": "You can't set a custom DNS server on some types of routers. In this case it may help if you set up <dhcp>AdGuard Home as a DHCP server</dhcp>. Otherwise, you should search for the manual on how to customize DNS servers for your particular router model."
|
||||
"install_configure_dhcp": "You can't set a custom DNS server on some types of routers. In this case it may help if you set up <dhcp>AdGuard Home as a DHCP server</dhcp>. Otherwise, you should search for the manual on how to customize DNS servers for your particular router model.",
|
||||
|
||||
"header_adguard_status_enabled": "AdGuard Home is enabled",
|
||||
"header_adguard_status_disabled": "AdGuard Home is disabled",
|
||||
"header_server_uptime": "Server uptime is %value%",
|
||||
|
||||
"top_clients": "Top clients",
|
||||
"client_table_header": "Client",
|
||||
"requests": "Requests",
|
||||
"show_blocked_responses": "Blocked",
|
||||
|
||||
"filter_category_general": "General",
|
||||
"query_log_configuration": "Logs configuration",
|
||||
"statistics_configuration": "Statistics configuration",
|
||||
"statistics_clear": " Clear statistics",
|
||||
"interval_24_hour": "24 hours",
|
||||
"interval_days": "| %count% day | %count% days",
|
||||
"interval_hours": "| %count% hour | %count% hours",
|
||||
"save_btn": "Save",
|
||||
"stats_reset": "Statistics reseted succesfully",
|
||||
"statistics_retention": "Statistics retention",
|
||||
"statistics_retention_desc": "If you decrease the interval value, some data will be lost",
|
||||
"query_log_enable": "Enable log",
|
||||
"query_log_clear": "Clear query logs",
|
||||
"query_log_retention": "Query logs retention",
|
||||
"query_log_cleared": "The query log has been successfully cleared",
|
||||
"anonymize_client_ip": "Anonymize client IP",
|
||||
"anonymize_client_ip_desc": "Don't save the full IP address of the client in logs and statistics",
|
||||
"query_log_retention_confirm": "If you decrease the interval value, some data will be lost",
|
||||
"query_log_confirm_clear": "Are you sure you want to clear the entire query log?",
|
||||
"query_log_reset": "Query log cleared succesfully",
|
||||
"statistics_clear_confirm": "Are you sure you want to clear statistics?",
|
||||
|
||||
"stats_query_domain": "Top queried domains",
|
||||
"top_blocked_domains": "Top blocked domains",
|
||||
"domain": "Domain",
|
||||
"all_queries": "All queries",
|
||||
|
||||
"block_domain_use_filters_and_hosts": "Block domains using filters and hosts files",
|
||||
"filters_interval": "Filters update interval",
|
||||
"filters_block_toggle_hint": "You can setup blocking rules in the <a>Filters</a> settings.",
|
||||
"use_adguard_browsing_sec": "Use AdGuard browsing security web service",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home will check if domain is blacklisted by the browsing security web service. It will use privacy-friendly lookup API to perform the check: only a short prefix of the domain name SHA256 hash is sent to the server.",
|
||||
"use_adguard_parental": "Use AdGuard parental control web service",
|
||||
"use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.",
|
||||
"enforce_safe_search": "Enforce safe search",
|
||||
"enforce_save_search_hint": "AdGuard Home can enforce safe search in the following search engines: Google, Youtube, Bing, DuckDuckGo, Yandex, Pixabay.",
|
||||
|
||||
"dashboard_blocked_ads": "Blocked Ads",
|
||||
"dashboard_blocked_trackers": "Blocked trackers",
|
||||
"dashboard_filter_rules": "Count of filter rules",
|
||||
"dashboard_blocked_queries": "Blocked queries",
|
||||
"dashboard_filter_rules_count": "%enabled% of %all% filters",
|
||||
"dashboard_server_statistics": "Internal server statistic",
|
||||
"other" : "Other",
|
||||
"ads" : "Ads",
|
||||
"trackers" : "Trackers",
|
||||
"stats_adult": "Blocked adult websites",
|
||||
"stats_malware_phishing": "Blocked malware/phishing",
|
||||
"average_processing_time": "Average processing time",
|
||||
"milliseconds_abbreviation": "ms"
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import 'dayjs/locale/ru';
|
||||
|
||||
import { PickerLocale } from 'antd/es/date-picker/generatePicker';
|
||||
@@ -22,15 +21,9 @@ export const messages: Record<Locale, Record<string, string>> = {
|
||||
[Locale.en]: enLang,
|
||||
};
|
||||
|
||||
// TODO get languages and default locale from .twosky file
|
||||
export const DEFAULT_LOCALE = Locale.en;
|
||||
|
||||
export const reactFormater = (data: (JSX.Element | string)[]) => {
|
||||
if (data.every((d) => typeof d === 'string')) {
|
||||
return data.join('');
|
||||
}
|
||||
return React.Children.toArray(data);
|
||||
};
|
||||
|
||||
export const LANGUAGES: { code: Locale; name: string }[] = [
|
||||
{
|
||||
code: Locale.en,
|
||||
|
||||
Reference in New Issue
Block a user