Skip to content

Commit 2c39c40

Browse files
feat: add support for glossary creation using CSV format
1 parent 911a4bd commit 2c39c40

File tree

4 files changed

+126
-22
lines changed

4 files changed

+126
-22
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77

8+
## [Unreleased]
9+
### Added
10+
* Add `createGlossaryWithCsv()` allowing glossaries downloaded from website to
11+
be easily uploaded to API.
12+
13+
814
## [1.3.2] - 2022-08-09
915
### Changed
1016
* Update contributing guidelines, we can now accept Pull Requests.
@@ -93,6 +99,7 @@ client library took over this package name. Thanks to
9399
ownership.
94100

95101

102+
[Unreleased]: https://github.com/DeepLcom/deepl-node/compare/v1.3.2..HEAD
96103
[1.3.2]: https://github.com/DeepLcom/deepl-node/compare/v1.3.1...v1.3.2
97104
[1.3.1]: https://github.com/DeepLcom/deepl-node/compare/v1.2.2...v1.3.1
98105
[1.3.0]: https://github.com/DeepLcom/deepl-node/releases/tag/v1.3.0

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,23 @@ const entries = new deepl.GlossaryEntries({ entries: { artist: 'Maler', prize: '
249249
const glossaryEnToDe = await translator.createGlossary('My glossary', 'en', 'de', entries);
250250
```
251251

252+
You can also upload a glossary downloaded from the DeepL website using
253+
`createGlossaryFromCsv()`. Instead of supplying the entries as a dictionary,
254+
provide the CSV file as a string containing the file path, or a Stream, Buffer,
255+
or FileHandle containing the CSV file content:
256+
257+
```javascript
258+
const csvFilePath = '/path/to/glossary_file.csv';
259+
const glossaryEnToDe = await translator.createGlossaryFromCsv(
260+
'My glossary',
261+
'en',
262+
'de',
263+
csvFilePath);
264+
```
265+
266+
The [API documentation][api-docs-csv-format] explains the expected CSV format in
267+
detail.
268+
252269
Functions to get, list, and delete stored glossaries are also provided.
253270

254271
```javascript
@@ -422,6 +439,7 @@ tests using `npm test` with the `DEEPL_MOCK_SERVER_PORT` and `DEEPL_SERVER_URL`
422439
environment variables defined referring to the mock-server.
423440

424441
[api-docs]: https://www.deepl.com/docs-api?utm_source=github&utm_medium=github-nodejs-readme
442+
[api-docs-csv-format]: https://www.deepl.com/docs-api/managing-glossaries/supported-glossary-formats/?utm_source=github&utm_medium=github-nodejs-readme
425443
[create-account]: https://www.deepl.com/pro?utm_source=github&utm_medium=github-nodejs-readme#developer
426444
[deepl-mock]: https://www.github.com/DeepLcom/deepl-mock
427445
[issues]: https://www.github.com/DeepLcom/deepl-node/issues

src/index.ts

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ export class Translator {
778778
}
779779

780780
/**
781-
* Creates a new glossary on DeepL server with given name, languages, and entries.
781+
* Creates a new glossary on the DeepL server with given name, languages, and entries.
782782
* @param name User-defined name to assign to the glossary.
783783
* @param sourceLang Language code of the glossary source terms.
784784
* @param targetLang Language code of the glossary target terms.
@@ -791,33 +791,43 @@ export class Translator {
791791
targetLang: LanguageCode,
792792
entries: GlossaryEntries,
793793
): Promise<GlossaryInfo> {
794-
// Glossaries are only supported for base language types
795-
sourceLang = nonRegionalLanguageCode(sourceLang);
796-
targetLang = nonRegionalLanguageCode(targetLang);
797-
798-
if (!isString(name) || name.length === 0) {
799-
throw new DeepLError('glossary name must be a non-empty string');
800-
} else if (Object.keys(entries.entries()).length === 0) {
794+
if (Object.keys(entries.entries()).length === 0) {
801795
throw new DeepLError('glossary entries must not be empty');
802796
}
803797

804798
const tsv = entries.toTsv();
799+
return this.internalCreateGlossary(name, sourceLang, targetLang, 'tsv', tsv);
800+
}
805801

806-
const data = new URLSearchParams({
807-
name: name,
808-
source_lang: sourceLang,
809-
target_lang: targetLang,
810-
entries_format: 'tsv',
811-
entries: tsv,
812-
});
802+
/**
803+
* Creates a new glossary on DeepL server with given name, languages, and CSV data.
804+
* @param name User-defined name to assign to the glossary.
805+
* @param sourceLang Language code of the glossary source terms.
806+
* @param targetLang Language code of the glossary target terms.
807+
* @param csvFile String containing the path of the CSV file to be translated, or a Stream,
808+
* Buffer, or a FileHandle containing CSV file content.
809+
* @return Fulfills with a GlossaryInfo containing details about the created glossary.
810+
*/
811+
async createGlossaryWithCsv(
812+
name: string,
813+
sourceLang: LanguageCode,
814+
targetLang: LanguageCode,
815+
csvFile: string | Buffer | fs.ReadStream | fs.promises.FileHandle,
816+
): Promise<GlossaryInfo> {
817+
let csvContent;
818+
if (isString(csvFile)) {
819+
csvContent = (await fs.promises.readFile(csvFile)).toString();
820+
} else if (csvFile instanceof fs.ReadStream) {
821+
csvContent = (await streamToBuffer(csvFile)).toString();
822+
} else if (csvFile instanceof Buffer) {
823+
csvContent = csvFile.toString();
824+
} else {
825+
// FileHandle
826+
csvContent = (await csvFile.readFile()).toString();
827+
await csvFile.close();
828+
}
813829

814-
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff<string>(
815-
'POST',
816-
'/v2/glossaries',
817-
{ data },
818-
);
819-
await checkStatusCode(statusCode, content, true);
820-
return parseGlossaryInfo(content);
830+
return this.internalCreateGlossary(name, sourceLang, targetLang, 'csv', csvContent);
821831
}
822832

823833
/**
@@ -932,6 +942,42 @@ export class Translator {
932942
});
933943
}
934944

945+
/**
946+
* Create glossary with given details.
947+
* @private
948+
*/
949+
private async internalCreateGlossary(
950+
name: string,
951+
sourceLang: LanguageCode,
952+
targetLang: LanguageCode,
953+
entriesFormat: string,
954+
entries: string,
955+
): Promise<GlossaryInfo> {
956+
// Glossaries are only supported for base language types
957+
sourceLang = nonRegionalLanguageCode(sourceLang);
958+
targetLang = nonRegionalLanguageCode(targetLang);
959+
960+
if (!isString(name) || name.length === 0) {
961+
throw new DeepLError('glossary name must be a non-empty string');
962+
}
963+
964+
const data = new URLSearchParams({
965+
name: name,
966+
source_lang: sourceLang,
967+
target_lang: targetLang,
968+
entries_format: entriesFormat,
969+
entries: entries,
970+
});
971+
972+
const { statusCode, content } = await this.httpClient.sendRequestWithBackoff<string>(
973+
'POST',
974+
'/v2/glossaries',
975+
{ data },
976+
);
977+
await checkStatusCode(statusCode, content, true);
978+
return parseGlossaryInfo(content);
979+
}
980+
935981
/**
936982
* HttpClient implements all HTTP requests and retries.
937983
* @private

tests/glossary.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,39 @@ describe('translate using glossaries', () => {
8080
}
8181
});
8282

83+
it('should create glossaries from CSV', async () => {
84+
const translator = makeTranslator();
85+
const glossaryName = getGlossaryName();
86+
const sourceLang = 'en';
87+
const targetLang = 'de';
88+
89+
const expectedEntries = new deepl.GlossaryEntries({
90+
entries: {
91+
sourceEntry1: 'targetEntry1',
92+
'source"Entry': 'target,Entry',
93+
},
94+
});
95+
const csvFile = Buffer.from(
96+
'sourceEntry1,targetEntry1,en,de\n"source""Entry","target,Entry",en,de',
97+
);
98+
const glossary = await translator.createGlossaryWithCsv(
99+
glossaryName,
100+
sourceLang,
101+
targetLang,
102+
csvFile,
103+
);
104+
try {
105+
const entries = await translator.getGlossaryEntries(glossary);
106+
expect(entries.entries()).toStrictEqual(expectedEntries.entries());
107+
} finally {
108+
try {
109+
await translator.deleteGlossary(glossary);
110+
} catch (e) {
111+
// Suppress errors
112+
}
113+
}
114+
});
115+
83116
it('should reject creating invalid glossaries', async () => {
84117
const translator = makeTranslator();
85118
const glossaryName = getGlossaryName();

0 commit comments

Comments
 (0)