Skip to content

Commit

Permalink
Add custom function support for case matching
Browse files Browse the repository at this point in the history
Update readome
Move types out to type files
  • Loading branch information
brocococonut committed Sep 25, 2023
1 parent d62b931 commit 4c2d18b
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 162 deletions.
300 changes: 191 additions & 109 deletions README.md

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
}
},
"test": {
"files": {
"include": [],
"exclude": ["npm_build/"]
}
"include": ["test/"],
"exclude": ["npm_build/"]
},
"bench": {
"include": ["test/"],
"exclude": ["npm_build/"]
},
"compilerOptions": {}
}
9 changes: 7 additions & 2 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FunctionType } from "./types/fn.ts";
import { format } from "./util/format.ts";
import { flattenObject } from "./util/obj.ts";

Expand Down Expand Up @@ -138,14 +139,18 @@ export class LocaleKit {
* @param opts Options for the translation (lang, data, etc.)
* @returns The translated string
*/
t(key: string, opts?: Record<string, unknown>) {
t<T extends Record<string, unknown>>(
key: string,
opts?: T,
functions?: Record<string, FunctionType<T>>,
) {
// Make sure the langauge is supported
const found = this.getKey(
(opts?.lang || this.fallback_language) as string,
key,
);

return format(found, opts || {});
return format(found, opts || ({} as T), functions || {});
}

/**
Expand Down
51 changes: 51 additions & 0 deletions test/translation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from "https://deno.land/std@0.160.0/testing/asserts.ts";

import { LocaleKit } from "../mod.ts";
import { FunctionType } from "../types/fn.ts";

Deno.test("Init with language", () => {
const svc = new LocaleKit({
Expand Down Expand Up @@ -524,3 +525,53 @@ Deno.test({ name: "Translate a key and handle function calls" }, () => {
"You have too many messages",
);
});

// Create a test for when a case has a function argument
Deno.test(
{
name:
"Translate a key and handle function calls that have function parameters",
},
() => {
const svc = new LocaleKit();

svc.addLanguage("en", {
common: {
test_age:
"You are [[~ {age} CUSTOM(fn: {isChild}): `a child` | CUSTOM(fn: {isTeen}): `a teenager` | CUSTOM(fn: {isAdult}): `an adult` ]]",
},
});

const fns: Record<string, FunctionType> = {
isChild: (age: unknown, _ctx, _matched) => {
return (age as number) <= 12;
},
isTeen: (age: unknown, _ctx, _matched) => {
return (age as number) > 12 && (age as number) < 18;
},
isAdult: (age: unknown, _ctx, _matched) => {
return (age as number) >= 18;
},
};

assertEquals(
svc.t("common.test_age", { age: 18 }, fns),
"You are an adult",
);
assertEquals(
svc.t("common.test_age", { age: 13 }, fns),
"You are a teenager",
);
assertEquals(svc.t("common.test_age", { age: 8 }, fns), "You are a child");
assertEquals(svc.t("common.test_age", { age: 0 }, fns), "You are a child");
assertEquals(svc.t("common.test_age", { age: 12 }, fns), "You are a child");
assertEquals(
svc.t("common.test_age", { age: 17 }, fns),
"You are a teenager",
);
assertEquals(
svc.t("common.test_age", { age: 18 }, fns),
"You are an adult",
);
},
);
7 changes: 7 additions & 0 deletions types/fn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type FunctionType<
T extends Record<string, unknown> = Record<string, unknown>,
> = (
val: unknown,
ctx: T,
matched: string,
) => unknown;
18 changes: 18 additions & 0 deletions types/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type ArgType = number | string;

export type FuncParamType = "num" | "str" | "key" | "bool" | "fn";

/**
* A map of functions, how many args and their types they take, and their names
*/
export interface FUNCSMapType {
[func_name: string]: {
arg_count: number;
arg_types: [
(FuncParamType)[],
(FuncParamType)[]?,
];
// deno-lint-ignore no-explicit-any
func: (...args: any) => boolean;
};
}
57 changes: 41 additions & 16 deletions util/format.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,45 @@
import { getNestedKeyValue } from "./obj.ts";
import { FUNC_NAMES, FUNCS, getFunctionParameters } from "./function.ts";
import { FunctionType } from "../types/fn.ts";
import { ArgType } from "../types/format.ts";

const DYN_STR_REGEX =
/\[\[~\s*(?:{(?<data_key>.*?)})\s*(?<cases>(?:\s*(?<case_key>(?:(?:[\w-])|(?:(?:LEN_)?N?GTE?|(?:LEN_)?N?LTE?|(?:LEN_)?N?EQ|(?:LEN_)?N?BT|(?:LEN_)?N?IN|(?<!LEN_)X?OR|(?<!LEN_)AND)\((?:[^)]+)\))+)\s*:\s*(?:(?:`[^`]*`)|(?:;:(?:(?!:;).)*:;))\s*\|*\s*)+)+\]\]/gs;
/\[\[~\s*(?:{(?<data_key>.*?)})\s*(?<cases>(?:\s*(?<case_key>(?:(?:[\w-])|(?:(?:LEN_)?N?GTE?|(?:LEN_)?N?LTE?|(?:LEN_)?N?EQ|(?:LEN_)?N?BT|(?:LEN_)?N?IN|CUSTOM|(?:LEN_)?X?OR|(?:LEN_)?AND)\((?:[^)]+)\))+)\s*:\s*(?:(?:`[^`]*`)|(?:;:(?:(?!:;).)*:;))\s*\|*\s*)+)+\]\]/gs;
const DYN_CASEKEY_REGEX =
/(?<case_key>(?:(?:[\w-])|(?:(?:LEN_)?N?GTE?|(?:LEN_)?N?LTE?|(?:LEN_)?N?EQ|(?:LEN_)?N?BT|(?:LEN_)?N?IN|(?<!LEN_)X?OR|(?<!LEN_)AND)\((?:[^)]+)\))+)\s*:\s*(?:(?:`(?<content_a>[^`]*)`)|(?:;:(?<content_b>(?:(?!:;).)*):;))/gs;
/(?<case_key>(?:(?:[\w-])|(?:(?:LEN_)?N?GTE?|(?:LEN_)?N?LTE?|(?:LEN_)?N?EQ|(?:LEN_)?N?BT|(?:LEN_)?N?IN|CUSTOM|(?:LEN_)?X?OR|(?:LEN_)?AND)\((?:[^)]+)\))+)\s*:\s*(?:(?:`(?<content_a>[^`]*)`)|(?:;:(?<content_b>(?:(?!:;).)*):;))/gs;

/**
* Function for handling
*/
const DYN_FUNC_REGEX =
/(?<func>(?:LEN_)?N?GTE?|(?:LEN_)?N?LTE?|(?:LEN_)?N?EQ|(?:LEN_)?N?BT|(?:LEN_)?N?IN|(?<!LEN_)X?OR|(?<!LEN_)AND)\((?<args>[^)]+)\)/s;
/(?<func>(?:LEN_)?N?GTE?|(?:LEN_)?N?LTE?|(?:LEN_)?N?EQ|(?:LEN_)?N?BT|(?:LEN_)?N?IN|CUSTOM|(?:LEN_)?X?OR|(?:LEN_)?AND)\((?<args>[^)]+)\)/s;

/**
* Regex for inner content replacement
*/
const DYN_NESTED_REGEX = /\{\{(.*?)\}\}(?:\|\|(.*?)\|\|)?/gs;

export function format(str = "", opts: Record<string, unknown> = {}) {
export function format<T extends Record<string, unknown>>(
str = "",
data?: T,
functions?: Record<string, FunctionType<T>>,
) {
const ctx = data || ({} as T);

// Find anything matching something similar to [[~ {object.nested.key} 1: `string` | 2: `{{object.second.nested.key}} string` | 3: `string` | ... | default: `string` ]]
// and replace it with the correct string depending on the value of the object.nested.key
const translated = str.replaceAll(
DYN_STR_REGEX,
(
_matched_str: string,
matched_str: string,
_key: string,
_dyn_field: string,
_case_key: string,
_unk,
_src_str: string,
groups: { data_key: string; case_key: string; cases: string },
) => {
let cur_val = getNestedKeyValue(opts, groups["data_key"]);
let cur_val = getNestedKeyValue(ctx, groups["data_key"]);

if (cur_val === undefined) {
cur_val = "default";
Expand Down Expand Up @@ -66,9 +74,14 @@ export function format(str = "", opts: Record<string, unknown> = {}) {
return;
}

const [arg1_obj, arg2_obj] = getFunctionParameters(key, opts);
const arg1 = arg1_obj?.val;
const arg2 = arg2_obj?.val;
// Fetch the function parameters
const [arg1_obj, arg2_obj] = getFunctionParameters(
key,
ctx,
functions,
);
let arg1 = arg1_obj?.val;
let arg2 = arg2_obj?.val;
const arg1_type = arg1_obj?.type;
const arg2_type = arg2_obj?.type;

Expand Down Expand Up @@ -99,13 +112,25 @@ export function format(str = "", opts: Record<string, unknown> = {}) {
return;
}

if (arg1_type === "fn") {
const fn_to_be_wrapped = arg1 as FunctionType<T>;
// Wrap the function in a new function that returns a boolean, also give the wrapped function a callback to return a value
arg1 = (a: unknown) => {
return !!fn_to_be_wrapped(a, ctx, matched_str);
};
}

if (arg2_type === "fn") {
const fn_to_be_wrapped = arg2 as FunctionType<T>;
arg2 = (a: unknown) => {
return !!fn_to_be_wrapped(a, ctx, matched_str);
};
}

// If the types are valid, run the function
const result =
(func_obj.func as (a: string, b: string, c: string) => boolean)(
cur_val as string,
arg1 as string,
arg2 as string,
);
const result = (
func_obj.func as (a: string, b: string, c: string) => boolean
)(cur_val as string, arg1 as string, arg2 as string);

// If the result is true, set the value of this key to the
// external func_case_val variable for further use
Expand Down Expand Up @@ -140,7 +165,7 @@ export function format(str = "", opts: Record<string, unknown> = {}) {
const formatted = translated.replaceAll(
DYN_NESTED_REGEX,
(_match, key, fallback) => {
const val = getNestedKeyValue(opts, key);
const val = getNestedKeyValue(ctx, key);
if (val === undefined) {
return fallback || "[no_value]";
}
Expand Down

0 comments on commit 4c2d18b

Please sign in to comment.