Skip to content

Commit

Permalink
Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
jarda-svoboda committed Feb 4, 2022
1 parent bf6e6c0 commit 8813ca7
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 164 deletions.
35 changes: 18 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { derived, get, writable } from 'svelte/store';
import { fetchTranslations, testRoute, toDotNotation, translate } from './utils';
import { useDefault as d, useDefault } from './utils/common';
import { fetchTranslations, testRoute, toDotNotation, useDefault as d } from './utils';
import parser from './parser/parser';

import type { Config, ConfigTranslations, CustomModifiers, LoaderModule, LoadingStore, LocalTranslationFunction, Route, TranslationFunction, Translations, ExtendedStore } from './types';
import type { Config, ConfigTranslations, LoaderModule, LoadingStore, LocalTranslationFunction, Route, TranslationFunction, Translations, ExtendedStore } from './types';
import type { CustomModifiers } from './parser/types';
import type { Readable, Writable } from 'svelte/store';

export { Config };
Expand Down Expand Up @@ -71,13 +72,13 @@ export default class {

t: ExtendedStore<TranslationFunction, TranslationFunction> = {
...derived(
[this.translation, this.config],
([$translation, { customModifiers, fallbackLocale }]): TranslationFunction => (key, vars) => translate({
translation: $translation,
translations: this.translations.get(),
key,
vars,
[this.config, this.translation],
([{ customModifiers, fallbackLocale }]): TranslationFunction => (key, payload) => parser({
customModifiers: d<CustomModifiers>(customModifiers),
}).parse({
key,
payload,
translations: this.translations.get(),
locale: this.locale.get(),
fallbackLocale,
}),
Expand All @@ -87,14 +88,14 @@ export default class {

l: ExtendedStore<LocalTranslationFunction, LocalTranslationFunction> = {
...derived(
[this.translations, this.config],
([$translations, { customModifiers, fallbackLocale }]): LocalTranslationFunction => (locale, key, vars) => translate({
translation: $translations[locale],
translations: $translations,
key,
vars,
[this.config, this.translations],
([{ customModifiers, fallbackLocale }, translations]): LocalTranslationFunction => (locale, key, payload) => parser({
customModifiers: d<CustomModifiers>(customModifiers),
locale: this.locale.get(),
}).parse({
key,
payload,
translations,
locale,
fallbackLocale,
}),
),
Expand Down Expand Up @@ -201,7 +202,7 @@ export default class {
);

const keys = filteredLoaders
.filter(({ key, locale }) => useDefault<string[]>(loadedKeys[locale], []).some(
.filter(({ key, locale }) => d<string[]>(loadedKeys[locale], []).some(
(loadedKey) => `${loadedKey}`.startsWith(key),
))
.reduce<Record<string, any>>((acc, { key, locale }) => ({
Expand Down
2 changes: 1 addition & 1 deletion src/modifiers.ts → src/parser/modifiers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useDefault, findOption } from './utils/common';
import { useDefault, findOption } from '../utils';
import type { Modifier, ModifierOption } from './types';

export const eq: Modifier = (value, options = [], defaultValue = '') => useDefault(options.find(
Expand Down
82 changes: 82 additions & 0 deletions src/parser/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as defaultModifiers from './modifiers';
import { useDefault } from '../utils';

import type { CustomModifiers, ModifierOption, Parser } from './types';

const hasPlaceholders = (text:string = '') => /{{(?:(?!{{|}}).)+}}/.test(`${text}`);

const unesc = (text:string) => text.replace(/\\(?=:|;|{|})/g, '');

const placeholders = (text: string, vars: Record<any, any> = {}, customModifiers: CustomModifiers = {}, locale?: string) => text.replace(/{{\s*(?:(?!{{|}}).)+\s*}}/g, (placeholder: string) => {
const key = unesc(`${placeholder.match(/(?!{|\s).+?(?!\\[:;]).(?=\s*(?:[:;]|}}$))/)}`);
const value = vars?.[key];
const [,defaultValue = ''] = useDefault(placeholder.match(/.+?(?!\\;).;\s*default\s*:\s*([^\s:;].+?(?:\\[:;]|[^;\s}])*)(?=\s*(?:;|}}$))/i), []);

let [,modifierKey = ''] = useDefault(placeholder.match(/{{\s*(?:[^;]|(?:\\;))+\s*(?:(?!\\:).[:])\s*(?!\s)((?:\\;|[^;])+?)(?=\s*(?:[;]|}}$))/i), []);

if (value === undefined && modifierKey !== 'ne') return defaultValue;

const hasModifier = !!modifierKey;

const modifiers: CustomModifiers = { ...defaultModifiers, ...useDefault(customModifiers) };

modifierKey = Object.keys(modifiers).includes(modifierKey) ? modifierKey : 'eq';

const modifier = modifiers[modifierKey];
const options: ModifierOption[] = useDefault<any[]>(
placeholder.match(/[^\s:;{](?:[^;]|\\[;])+[^\s:;}]/gi), [],
).reduce(
(acc, option, i) => {
// NOTE: First item is placeholder and modifier
if (i > 0) {
const optionKey = unesc(`${option.match(/(?:(?:\\:)|[^:])+/)}`.trim());
const optionValue = `${option.match(/(?:(?:\\:)|[^:])+$/)}`.trim();

if (optionKey && optionKey !== 'default' && optionValue) return ([ ...acc, { key: optionKey, value: optionValue }]);
}

return acc;
}, [],
);

if (!hasModifier && !options.length) return `${value}`;

return modifier(value, options, defaultValue, locale);

});

const interpolate = (text: string, vars: Record<any, any> = {}, customModifiers?: CustomModifiers, locale?: string):string => {
if (hasPlaceholders(text)) {
const output = placeholders(text, vars, customModifiers, locale);

return interpolate(output, vars, customModifiers, locale);
} else {
return unesc(`${text}`);
}
};


const parser: Parser = ({ customModifiers = {} }) => ({
parse: ({ translations = {}, key, payload, locale, fallbackLocale }) => {
if (!key) throw new Error('No key provided to $t()');
if (!locale) throw new Error('No locale set!');

let text = useDefault(translations[locale])[key];

if (fallbackLocale && text === undefined) {
text = useDefault(translations[fallbackLocale])[key];
}

if (payload?.default && text === undefined) {
text = `${payload.default}`;
}

if (text === undefined) {
text = `${key}`;
}

return interpolate(text, payload, customModifiers, locale);
},
});

export default parser;
22 changes: 22 additions & 0 deletions src/parser/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type ModifierKey = 'lt' | 'lte' | 'eq' | 'gte' | 'gt';

export type ModifierOption = Record<'key' | 'value', string>;

export type Modifier = (value: any, options:ModifierOption[], defaultValue?: string, locale?: string) => string;

export type DefaultModifiers = Record<ModifierKey, Modifier>;

export type CustomModifiers = Record<string, Modifier>;

export type Parser = (config: {
customModifiers?: CustomModifiers;
}) => {
fallbackLocale?: string,
parse: (props: {
translations: Record<string, Record<string, any>>;
key: string;
payload?: Record<any, any>;
locale?: string;
fallbackLocale?: string;
}) => string
};
24 changes: 3 additions & 21 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Readable } from 'svelte/store';

import type { CustomModifiers } from './parser/types';

export type LoadingStore = Readable<boolean> & { toPromise: () => Promise<void[]>, get: () => boolean };

export type Loader = () => Promise<Record<any, any>>;
Expand All @@ -21,16 +23,6 @@ export type ToDotNotation = (input: DotNotationInput, parentKey?: string) => Dot

export type FetchTranslations = (loaders: LoaderModule[]) => Promise<Record<string, DotNotationOutput>>;

export type Translate = (props: {
translation: DotNotationOutput;
translations: Record<string, DotNotationOutput>;
key: string;
vars?: Record<any, any>;
customModifiers?: CustomModifiers;
locale?: string;
fallbackLocale?: string;
}) => string;

export type TranslationFunction = (key: string, vars?: Record<any, any>) => string;

export type LocalTranslationFunction = (locale: string, key: string, vars?: Record<any, any>) => string;
Expand All @@ -49,14 +41,4 @@ export type Config = {
customModifiers?: CustomModifiers;
};

export type GetConfig = (...params: any) => Config;

export type ModifierKey = 'lt' | 'lte' | 'eq' | 'gte' | 'gt';

export type ModifierOption = Record<'key' | 'value', string>;

export type Modifier = (value: any, options:ModifierOption[], defaultValue?: string, locale?: string) => string;

export type DefaultModifiers = Record<ModifierKey, Modifier>;

export type CustomModifiers = Record<string, Modifier>;
export type GetConfig = (...params: any) => Config;
50 changes: 50 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ToDotNotation, FetchTranslations, Route, LoaderModule } from './types';
import type { ModifierOption } from './parser/types';

export const useDefault = <T = any>(value: any, def:any = {}): T => value || def;

export const toDotNotation: ToDotNotation = (input, parentKey) => Object.keys(useDefault(input)).reduce((acc, key) => {
const value = input[key];
const outputKey = parentKey ? `${parentKey}.${key}` : `${key}`;

if (value && typeof value === 'object') return ({ ...acc, ...toDotNotation(value, outputKey) });

return ({ ...acc, [outputKey]: value });
}, {});

export const fetchTranslations: FetchTranslations = async (loaders) => {
try {
const data = await Promise.all(loaders.map(({ loader, ...rest }) => new Promise<LoaderModule & { data: any }>(async (res) => {
let data;
try {
data = await loader();
} catch (error) {
console.error(`Failed to load translation. Verify your '${rest.locale}' > '${rest.key}' loader.`);
console.error(error);
}
res({ loader, ...rest, data });
})));

return data.reduce<Record<string, any>>((acc, { key, data, locale }) => data ? ({
...acc,
[locale]: toDotNotation({ ...useDefault<Record<any, any>>(acc[locale]), [key]: data }),
}) : acc, {});
} catch (error) {
console.error(error);
}

return {};
};

export const testRoute = (route: string) => (input: Route) => {
try {
if (typeof input === 'string') return input === route;
if (typeof input === 'object') return input.test(route);
} catch (error) {
throw new Error('Invalid route config!');
}

return false;
};

export const findOption = <T = string>(options: ModifierOption[], key: string, defaultValue?: string): T => ((options.find((option) => option.key === key))?.value || defaultValue) as any;
5 changes: 0 additions & 5 deletions src/utils/common.ts

This file was deleted.

120 changes: 0 additions & 120 deletions src/utils/index.ts

This file was deleted.

0 comments on commit 8813ca7

Please sign in to comment.