Skip to content

Commit 6c76ae7

Browse files
authored
Merge pull request #6 from samchon/feat/texts
Enhance `JsonTranslator.detect()` function.
2 parents 1a09441 + b1239d7 commit 6c76ae7

File tree

6 files changed

+305
-28
lines changed

6 files changed

+305
-28
lines changed

README.md

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,136 @@ Also, below is the list of example result files of such translation.
6262
English (source) | Korean | Japanese | Arabic
6363
--------|--------|----------|--------
6464
[bbs.article.json](https://github.com/samchon/json-translator/blob/master/assets/input/bbs.article.json) | [bbs.ko.json](https://github.com/samchon/json-translator/blob/master/assets/output/bbs.article.ko.json) | [bbs.ja.json](https://github.com/samchon/json-translator/blob/master/assets/output/bbs.article.ja.json) | [bbs.ar.json](https://github.com/samchon/json-translator/blob/master/assets/output/bbs.article.ar.json)
65-
[shopping.swagger.json](https://github.com/samchon/json-translator/blob/master/assets/input/shopping.swagger.json) | [shopping.ko.json](https://github.com/samchon/json-translator/blob/master/assets/output/shopping.swagger.ko.json) | [shopping.ja.json](https://github.com/samchon/json-translator/blob/master/assets/output/shopping.swagger.ja.json) | [shopping.ar.json](https://github.com/samchon/json-translator/blob/master/assets/output/shopping.swagger.ar.json)
65+
[shopping.swagger.json](https://github.com/samchon/json-translator/blob/master/assets/input/shopping.swagger.json) | [shopping.ko.json](https://github.com/samchon/json-translator/blob/master/assets/output/shopping.swagger.ko.json) | [shopping.ja.json](https://github.com/samchon/json-translator/blob/master/assets/output/shopping.swagger.ja.json) | [shopping.ar.json](https://github.com/samchon/json-translator/blob/master/assets/output/shopping.swagger.ar.json)
66+
67+
68+
69+
70+
## API
71+
```typescript
72+
export class JsonTranslator {
73+
/**
74+
* Translate JSON data.
75+
*
76+
* `JsonTranslate.translate()` translates JSON input data into another language.
77+
*
78+
* If you want to filter some specific values to translate, fill the
79+
* {@link JsonTranslator.IProps.filter} function.
80+
*
81+
* Also, if you do not fill the {@link JsonTranslator.IProps.source} value,
82+
* the source language would be detected through the {@link JsonTranslator.detect}
83+
* method with the longest text. Otherwise you assign the `null` value to the
84+
* {@link JsonTranslator.IProps.source}, the translation would be executed without
85+
* the source language.
86+
*
87+
* @template T The type of the JSON input data.
88+
* @param props Properties for the translation.
89+
* @returns The translated JSON data.
90+
*/
91+
public translate<T>(props: JsonTranslator.IProps<T>): Promise<T>;
92+
93+
/**
94+
* Detect the language of JSON data.
95+
*
96+
* Pick the longest text from the JSON input data and detect the language
97+
* through the Google Translate API, with the similar properties like the
98+
* {@link JsonTranslator.translate} method.
99+
*
100+
* Therefore, if you want to filter some specific values to participate in
101+
* the language detection, fill the {@link JsonTranslator.IProps.filter}
102+
* function.
103+
*
104+
* @param input Properties for language detection.
105+
* @returns The detected language or `undefined` if the language is unknown.
106+
*/
107+
public detect<T>(
108+
props: Omit<JsonTranslator.IProps<T>, "source" | "target">,
109+
): Promise<string | undefined>;
110+
}
111+
export namespace JsonTranslator {
112+
/**
113+
* Properties for the translation.
114+
*/
115+
export interface IProps<T> {
116+
/**
117+
* The JSON input data to translate.
118+
*/
119+
input: T;
120+
121+
/**
122+
* Source language code.
123+
*
124+
* If not specified (`undefined`), the source language would be detected
125+
* through the {@link JsonTranslator.detect} method with the longest text.
126+
*
127+
* Otherwise `null` value assigned, the source language would be skipped,
128+
* so that the translation would be executed without the source language.
129+
* Therefore, if your JSON value contains multiple languages, you should
130+
* assign the `null` value to prevent the source language specification.
131+
*/
132+
source?: string | null | undefined;
133+
134+
/**
135+
* Target language code.
136+
*/
137+
target: string;
138+
139+
/**
140+
* Filter function specifying which data to translate.
141+
*
142+
* @param explore Information about the data to explore.
143+
* @returns `true` if the data should be translated; otherwise, `false`.
144+
*/
145+
filter?: ((explore: IExplore) => boolean) | null | undefined;
146+
147+
/**
148+
* Reserved dictionary of pre-translated values.
149+
*
150+
* The dictionary is a key-value pair object containing the pre-translated
151+
* values. The key means the original value, and the value means the
152+
* pre-translated value.
153+
*
154+
* If this dictionary has been configured and a JSON input value matches to
155+
* the dictionary's key, the dictionary's value would be used instead of
156+
* calling the Google Translate API.
157+
*/
158+
dictionary?: Record<string, string> | null | undefined;
159+
}
160+
161+
/**
162+
* Exploration information used in the {@link IProps.filter} function.
163+
*/
164+
export interface IExplore {
165+
/**
166+
* The parent object instance.
167+
*/
168+
object: object | null;
169+
170+
/**
171+
* The property key containing the {@link value}
172+
*/
173+
key: string | null;
174+
175+
/**
176+
* Index number if the {@link value} is an array element.
177+
*/
178+
index: number | null;
179+
180+
/**
181+
* Accessor path to the {@link value}.
182+
*
183+
* It starts from the `["$input"]` array value, and each element
184+
* would be the property key or the index number.
185+
*
186+
* For example, if there's an access expression `$input.a[0].b`,
187+
* the accessor would be `["$input", "a", "0", "b"]`.
188+
*/
189+
accessor: string[];
190+
191+
/**
192+
* The string value to translate.
193+
*/
194+
value: string;
195+
}
196+
}
197+
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@samchon/json-translator",
3-
"version": "1.2.0",
3+
"version": "2.0.0",
44
"description": "JSON Translator via Google Translation API with optimization strategies",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",

src/JsonTranslator.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class JsonTranslator {
4747
const from: string | undefined =
4848
props.source === null
4949
? undefined
50-
: (props.source ?? (await this.detect(collection.raw)));
50+
: (props.source ?? (await this._Detect_language(collection.raw)));
5151

5252
let queue: Array<IPiece> = [];
5353
let bytes: number = 0;
@@ -94,19 +94,35 @@ export class JsonTranslator {
9494
}
9595

9696
/**
97-
* Detect the language of the texts.
97+
* Detect the language of JSON data.
9898
*
99-
* @param texts Texts to detect the language.
99+
* Pick the longest text from the JSON input data and detect the language
100+
* through the Google Translate API, with the similar properties like the
101+
* {@link JsonTranslator.translate} method.
102+
*
103+
* Therefore, if you want to filter some specific values to participate in
104+
* the language detection, fill the {@link JsonTranslator.IProps.filter}
105+
* function.
106+
*
107+
* @param input Properties for language detection.
100108
* @returns The detected language or `undefined` if the language is unknown.
101109
*/
102-
public async detect(texts: string[]): Promise<string | undefined> {
103-
if (texts.length === 0) return undefined;
104-
const [response] = await this.service_.detect(
105-
texts
106-
.slice()
107-
.sort((a, b) => b.length - a.length)
108-
.at(0)!,
109-
);
110+
public async detect<T>(
111+
props: Omit<JsonTranslator.IProps<T>, "source" | "target">,
112+
): Promise<string | undefined> {
113+
const texts: string[] = JsonTranslateExecutor.getTexts(props);
114+
return this._Detect_language(texts);
115+
}
116+
117+
/**
118+
* @internal
119+
*/
120+
private async _Detect_language(texts: string[]): Promise<string | undefined> {
121+
let longest: string | undefined = undefined;
122+
for (const str of texts)
123+
if (str.length > (longest?.length ?? 0)) longest = str;
124+
if (longest === undefined) return undefined;
125+
const [response] = await this.service_.detect(longest);
110126
const res: string | undefined = response.language ?? undefined;
111127
return res === "und" ? undefined : res;
112128
}

src/internal/JsonTranslateExecutor.ts

Lines changed: 95 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { JsonTranslator } from "../JsonTranslator";
44
* @internal
55
*/
66
export namespace JsonTranslateExecutor {
7+
/* -----------------------------------------------------------
8+
PREPARE COLLECTION
9+
----------------------------------------------------------- */
710
export interface ICollection {
811
output: any;
912
raw: string[];
@@ -47,28 +50,32 @@ export namespace JsonTranslateExecutor {
4750
value: any;
4851
explore: Omit<JsonTranslator.IExplore, "value">;
4952
}): void => {
50-
if (typeof next.value === "string" && next.value.trim().length !== 0) {
51-
if (
53+
if (typeof next.value === "string") {
54+
if (next.value.trim().length === 0) return;
55+
else if (
5256
next.filter &&
5357
!next.filter({
5458
...next.explore,
5559
value: next.value,
5660
})
5761
)
5862
return;
59-
else if (next.dictionary && next.dictionary[next.value])
63+
else if (
64+
next.dictionary &&
65+
typeof next.dictionary[next.value] === "string"
66+
) {
6067
next.set(next.dictionary[next.value]);
61-
else {
62-
const found: Setter<string> | undefined = next.setters.get(next.value);
63-
if (found !== undefined) {
64-
next.setters.set(next.value, (str) => {
65-
next.set(str);
66-
found(str);
67-
});
68-
} else {
69-
next.raw.push(next.value);
70-
next.setters.set(next.value, next.set);
71-
}
68+
return;
69+
}
70+
const found: Setter<string> | undefined = next.setters.get(next.value);
71+
if (found !== undefined) {
72+
next.setters.set(next.value, (str) => {
73+
next.set(str);
74+
found(str);
75+
});
76+
} else {
77+
next.raw.push(next.value);
78+
next.setters.set(next.value, next.set);
7279
}
7380
} else if (Array.isArray(next.value))
7481
next.value.forEach((elem, i) =>
@@ -99,4 +106,78 @@ export namespace JsonTranslateExecutor {
99106
}),
100107
);
101108
};
109+
110+
/* -----------------------------------------------------------
111+
LIST UP TEXTS
112+
----------------------------------------------------------- */
113+
export const getTexts = (
114+
props: Omit<JsonTranslator.IProps<any>, "source" | "target">,
115+
): string[] => {
116+
const output: Set<string> = new Set();
117+
const visited: WeakSet<object> = new WeakSet();
118+
visitTexts({
119+
filter: props.filter,
120+
dictionary: props.dictionary ?? null,
121+
output,
122+
visited,
123+
value: props.input,
124+
explore: {
125+
object: null,
126+
key: null,
127+
index: null,
128+
accessor: ["$input"],
129+
},
130+
});
131+
return Array.from(output);
132+
};
133+
134+
const visitTexts = (next: {
135+
filter: JsonTranslator.IProps<any>["filter"];
136+
dictionary: Record<string, string> | null;
137+
output: Set<string>;
138+
visited: WeakSet<object>;
139+
value: any;
140+
explore: Omit<JsonTranslator.IExplore, "value">;
141+
}): void => {
142+
if (typeof next.value === "string") {
143+
if (next.value.length === 0) return;
144+
else if (
145+
next.filter &&
146+
!next.filter({
147+
...next.explore,
148+
value: next.value,
149+
})
150+
)
151+
return;
152+
else if (
153+
next.dictionary &&
154+
typeof next.dictionary[next.value] === "string"
155+
)
156+
return;
157+
else next.output.add(next.value);
158+
} else if (Array.isArray(next.value))
159+
next.value.forEach((elem, i) =>
160+
visitTexts({
161+
...next,
162+
value: elem,
163+
explore: {
164+
...next.explore,
165+
index: i,
166+
},
167+
}),
168+
);
169+
else if (typeof next.value === "object" && next.value !== null)
170+
Object.entries(next.value).forEach(([key, value]) =>
171+
visitTexts({
172+
...next,
173+
explore: {
174+
...next.explore,
175+
object: next.value,
176+
key,
177+
index: null,
178+
},
179+
value,
180+
}),
181+
);
182+
};
102183
}

test/features/test_texts.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { TestValidator } from "@nestia/e2e";
2+
import { JsonTranslateExecutor } from "@samchon/json-translator/lib/internal/JsonTranslateExecutor";
3+
4+
export const test_texts = (): void => {
5+
const input = {
6+
x: "x",
7+
y: "y",
8+
z: "z",
9+
nested: {
10+
alpha: "alpha",
11+
beta: "beta",
12+
reserved: "reserved",
13+
},
14+
exceptional: "exceptional",
15+
array: ["one", "two", "three"],
16+
elements: [
17+
{
18+
first: "first",
19+
second: "second",
20+
},
21+
{
22+
third: "third",
23+
fourth: "fourth",
24+
},
25+
],
26+
};
27+
const texts: string[] = JsonTranslateExecutor.getTexts({
28+
input,
29+
filter: (explore) => explore.key !== "exceptional",
30+
dictionary: {
31+
reserved: "reserved",
32+
},
33+
});
34+
TestValidator.equals("texts")(texts)([
35+
"x",
36+
"y",
37+
"z",
38+
"alpha",
39+
"beta",
40+
"one",
41+
"two",
42+
"three",
43+
"first",
44+
"second",
45+
"third",
46+
"fourth",
47+
]);
48+
};

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
5656
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
5757
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
58-
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
58+
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
5959
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
6060
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
6161
"outDir": "./lib", /* Specify an output folder for all emitted files. */

0 commit comments

Comments
 (0)