Pull request: beta client squashed

Merge in DNS/adguard-home from beta-client-2 to master

Squashed commit of the following:

commit b2640cc49a6c5484d730b534dcf5a8013d7fa478
Merge: 659def862 aef4659e9
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Dec 29 19:23:09 2020 +0300

    Merge branch 'master' into beta-client-2

commit 659def8626467949c35b7a6a0c99ffafb07b4385
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Dec 29 17:25:14 2020 +0300

    all: upgrade github actions node version

commit b4b8cf8dd75672e9155da5d111ac66e8f5ba1535
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date:   Tue Dec 29 16:57:14 2020 +0300

    all: beta client squashed
This commit is contained in:
Eugene Burkov
2020-12-29 19:53:56 +03:00
parent aef4659e93
commit 5e20ac7ed5
200 changed files with 20843 additions and 55 deletions

View File

@@ -0,0 +1,82 @@
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;

View File

@@ -0,0 +1,2 @@
export { default } from './Translator';
export { SupportedLangs, GenericLocales, AvailableLocales, checkFormsExternal as checkForms } from './lib/plural';

View File

@@ -0,0 +1,100 @@
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;

View File

@@ -0,0 +1,50 @@
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;
};

View File

@@ -0,0 +1,335 @@
/* 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;

View File

@@ -0,0 +1,394 @@
/* 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();
};

View File

@@ -0,0 +1,9 @@
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;

View File

@@ -0,0 +1,60 @@
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;
};

View File

@@ -0,0 +1,6 @@
import T from './Translator';
import { Locale } from './locales';
export { messages, DatePickerLocale, Locale, DEFAULT_LOCALE, LANGUAGES, reactFormater } from './locales';
export type Translator = T<Locale>;
export default T;

View File

@@ -0,0 +1,48 @@
{
"back": "Back",
"ethernet": "Ethernet",
"localhost": "localhost",
"login": "Login",
"password": "Password",
"next": "Next",
"port": "Port",
"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",
"install_admin_interface_title_decs": "Admin web interface is used to control AdGuard Home. You can open it in your browser and it does not require using a client-side program",
"install_admin_interface_title": "Admin interface settings",
"install_admin_interface_where_interface_desc": "Set what kind of networks will be able to access to Admin interface. For example: if you choose a local interface only, then Admin inteface will be accessed by this local device only",
"install_admin_interface_where_interface": "Where can I open Admin interface",
"install_all_networks_description": "All available web interfaces",
"install_all_networks": "All networks",
"install_choose_networks_desc": "For advanced users",
"install_choose_networks": "Choose manually",
"install_wellcome_button": "Let's go",
"install_wellcome_desc": "You have installed AdGuard Home on your device. It is a network-wide ad-and-tracker blocking DNS server with Admin Web interface. Lets set some settings to correct DNS working",
"install_wellcome_title": "Welcome to AdGuard Home",
"install_auth_title": "Login and password",
"install_auth_description": "Set login and password for accessing to Web interface",
"install_dns_server_title": "DNS server settings",
"install_dns_server_desc": "AdGuard DNS server works like common DNS server but also blocks ads and tracking domains",
"install_dns_server_network_interfaces": "Network interfaces",
"install_dns_server_network_interfaces_desc": "You should set for what kind of networks will be use AdGuard Home DNS server. Most often you need to have available all interfaces",
"install_dns_server_port": "Which port will be used",
"install_dns_server_port_desc": "You have to use port 53 for correct internet working. Change this value only if you have reason",
"install_dns_server_non_static_ip": "How to use non-static IP adresses?",
"install_configure_title": "Configure your devices",
"install_configure_danger_notice": "<danger>IMPORTANT!</danger> To start using AdGuard Home, you need to configure your devices manually",
"install_configure_how_to_title": "How to configure Router",
"install_configure_router": "<p>This setup will automatically cover all the devices connected to your home router and you will not need to configure each of them manually.</p> <p>Open the preferences for your router. Usually, you can access it from your browser via a URL (like http://192.168.0.1/ or http://192.168.1.1/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer/phone.</p><p>Find the DHCP/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.</p><p>Enter your AdGuard Home server addresses there.</p>",
"install_configure_windows": "<p>Open Control Panel through Start menu or Windows search.</p><p>Go to Network and Internet category and then to Network and Sharing Center.</p><p>On the left side of the screen find Change adapter settings and click on it.</p><p>Select your active connection, right-click on it and choose Properties.</p><p>Find Internet Protocol Version 4 (TCP/IP) in the list, select it and then click on Properties again.</p><p>Choose Use the following DNS server addresses and enter your AdGuard Home server addresses.</p>",
"install_configure_macos": "<p>Click on Apple icon and go to System Preferences.</p><p>Click on Network.</p><p>Select the first connection in your list and click Advanced.</p><p>Select the DNS tab and enter your AdGuard Home server addresses.</p>",
"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."
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import 'dayjs/locale/ru';
import { PickerLocale } from 'antd/es/date-picker/generatePicker';
import ruPicker from 'antd/es/date-picker/locale/ru_RU';
import enPicker from 'antd/es/date-picker/locale/en_GB';
import ruLang from './ru.json';
import enLang from './en.json';
export enum Locale {
en = 'en',
ru = 'ru',
}
export const DatePickerLocale: Record<Locale, PickerLocale> = {
[Locale.ru]: ruPicker,
[Locale.en]: enPicker,
};
export const messages: Record<Locale, Record<string, string>> = {
[Locale.ru]: ruLang,
[Locale.en]: enLang,
};
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,
name: 'English',
},
{
code: Locale.ru,
name: 'Русский',
},
];

View File

@@ -0,0 +1,4 @@
{
"install_wellcome_title": "Добро пожаловать в AdGuard Home",
"install_wellcome_desc": "Русский текст"
}