smartling-api-sdk-nodejs 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.env.example +1 -1
  2. package/api/translation-memory/dto/async-action-result-dto.ts +12 -0
  3. package/api/translation-memory/dto/async-action-status.ts +8 -0
  4. package/api/translation-memory/dto/create-string-search-result-dto.ts +16 -0
  5. package/api/translation-memory/dto/duplicate-string-dto.ts +9 -0
  6. package/api/translation-memory/dto/locale-summary-dto.ts +10 -0
  7. package/api/translation-memory/dto/strings-export-async-action-result-dto.ts +8 -0
  8. package/api/translation-memory/dto/tm-dto.ts +23 -0
  9. package/api/translation-memory/dto/tm-locale-dto.ts +8 -0
  10. package/api/translation-memory/dto/tmx-import-summary-dto.ts +16 -0
  11. package/api/translation-memory/dto/tmx-parsing-error-dto.ts +7 -0
  12. package/api/translation-memory/dto/user-dto.ts +6 -0
  13. package/api/translation-memory/index.ts +368 -0
  14. package/api/translation-memory/params/bulk-export-tm-strings-parameters.ts +30 -0
  15. package/api/translation-memory/params/create-string-search-filter-parameters.ts +232 -0
  16. package/api/translation-memory/params/create-string-search-options.ts +8 -0
  17. package/api/translation-memory/params/create-tm-parameters.ts +28 -0
  18. package/api/translation-memory/params/escape-smp-unicode-parser-directive.ts +6 -0
  19. package/api/translation-memory/params/extended-export-metadata-type.ts +4 -0
  20. package/api/translation-memory/params/import-tmx-file-parameters.ts +76 -0
  21. package/api/translation-memory/params/keyword-search-type.ts +5 -0
  22. package/api/translation-memory/params/leverage-type.ts +5 -0
  23. package/api/translation-memory/params/search-keyword.ts +10 -0
  24. package/api/translation-memory/params/source-type.ts +7 -0
  25. package/api/translation-memory/params/string-search-tags.ts +7 -0
  26. package/api/translation-memory/params/translation-quality-type.ts +5 -0
  27. package/api/translation-memory/params/tu-translation-quality-type.ts +6 -0
  28. package/api/translation-memory/params/update-tm-parameters.ts +23 -0
  29. package/built/api/translation-memory/dto/async-action-result-dto.d.ts +10 -0
  30. package/built/api/translation-memory/dto/async-action-result-dto.js +3 -0
  31. package/built/api/translation-memory/dto/async-action-result-dto.js.map +1 -0
  32. package/built/api/translation-memory/dto/async-action-status.d.ts +8 -0
  33. package/built/api/translation-memory/dto/async-action-status.js +13 -0
  34. package/built/api/translation-memory/dto/async-action-status.js.map +1 -0
  35. package/built/api/translation-memory/dto/create-string-search-result-dto.d.ts +11 -0
  36. package/built/api/translation-memory/dto/create-string-search-result-dto.js +3 -0
  37. package/built/api/translation-memory/dto/create-string-search-result-dto.js.map +1 -0
  38. package/built/api/translation-memory/dto/duplicate-string-dto.d.ts +8 -0
  39. package/built/api/translation-memory/dto/duplicate-string-dto.js +3 -0
  40. package/built/api/translation-memory/dto/duplicate-string-dto.js.map +1 -0
  41. package/built/api/translation-memory/dto/locale-summary-dto.d.ts +8 -0
  42. package/built/api/translation-memory/dto/locale-summary-dto.js +3 -0
  43. package/built/api/translation-memory/dto/locale-summary-dto.js.map +1 -0
  44. package/built/api/translation-memory/dto/strings-export-async-action-result-dto.d.ts +6 -0
  45. package/built/api/translation-memory/dto/strings-export-async-action-result-dto.js +3 -0
  46. package/built/api/translation-memory/dto/strings-export-async-action-result-dto.js.map +1 -0
  47. package/built/api/translation-memory/dto/tm-dto.d.ts +19 -0
  48. package/built/api/translation-memory/dto/tm-dto.js +3 -0
  49. package/built/api/translation-memory/dto/tm-dto.js.map +1 -0
  50. package/built/api/translation-memory/dto/tm-locale-dto.d.ts +7 -0
  51. package/built/api/translation-memory/dto/tm-locale-dto.js +3 -0
  52. package/built/api/translation-memory/dto/tm-locale-dto.js.map +1 -0
  53. package/built/api/translation-memory/dto/tmx-import-summary-dto.d.ts +11 -0
  54. package/built/api/translation-memory/dto/tmx-import-summary-dto.js +3 -0
  55. package/built/api/translation-memory/dto/tmx-import-summary-dto.js.map +1 -0
  56. package/built/api/translation-memory/dto/tmx-parsing-error-dto.d.ts +6 -0
  57. package/built/api/translation-memory/dto/tmx-parsing-error-dto.js +3 -0
  58. package/built/api/translation-memory/dto/tmx-parsing-error-dto.js.map +1 -0
  59. package/built/api/translation-memory/dto/user-dto.d.ts +5 -0
  60. package/built/api/translation-memory/dto/user-dto.js +3 -0
  61. package/built/api/translation-memory/dto/user-dto.js.map +1 -0
  62. package/built/api/translation-memory/index.d.ts +27 -0
  63. package/built/api/translation-memory/index.js +250 -0
  64. package/built/api/translation-memory/index.js.map +1 -0
  65. package/built/api/translation-memory/params/bulk-export-tm-strings-parameters.d.ts +8 -0
  66. package/built/api/translation-memory/params/bulk-export-tm-strings-parameters.js +24 -0
  67. package/built/api/translation-memory/params/bulk-export-tm-strings-parameters.js.map +1 -0
  68. package/built/api/translation-memory/params/create-string-search-filter-parameters.d.ts +43 -0
  69. package/built/api/translation-memory/params/create-string-search-filter-parameters.js +151 -0
  70. package/built/api/translation-memory/params/create-string-search-filter-parameters.js.map +1 -0
  71. package/built/api/translation-memory/params/create-string-search-options.d.ts +7 -0
  72. package/built/api/translation-memory/params/create-string-search-options.js +3 -0
  73. package/built/api/translation-memory/params/create-string-search-options.js.map +1 -0
  74. package/built/api/translation-memory/params/create-tm-parameters.d.ts +8 -0
  75. package/built/api/translation-memory/params/create-tm-parameters.js +24 -0
  76. package/built/api/translation-memory/params/create-tm-parameters.js.map +1 -0
  77. package/built/api/translation-memory/params/escape-smp-unicode-parser-directive.d.ts +4 -0
  78. package/built/api/translation-memory/params/escape-smp-unicode-parser-directive.js +9 -0
  79. package/built/api/translation-memory/params/escape-smp-unicode-parser-directive.js.map +1 -0
  80. package/built/api/translation-memory/params/extended-export-metadata-type.d.ts +4 -0
  81. package/built/api/translation-memory/params/extended-export-metadata-type.js +9 -0
  82. package/built/api/translation-memory/params/extended-export-metadata-type.js.map +1 -0
  83. package/built/api/translation-memory/params/import-tmx-file-parameters.d.ts +17 -0
  84. package/built/api/translation-memory/params/import-tmx-file-parameters.js +80 -0
  85. package/built/api/translation-memory/params/import-tmx-file-parameters.js.map +1 -0
  86. package/built/api/translation-memory/params/keyword-search-type.d.ts +5 -0
  87. package/built/api/translation-memory/params/keyword-search-type.js +10 -0
  88. package/built/api/translation-memory/params/keyword-search-type.js.map +1 -0
  89. package/built/api/translation-memory/params/leverage-type.d.ts +5 -0
  90. package/built/api/translation-memory/params/leverage-type.js +10 -0
  91. package/built/api/translation-memory/params/leverage-type.js.map +1 -0
  92. package/built/api/translation-memory/params/search-keyword.d.ts +8 -0
  93. package/built/api/translation-memory/params/search-keyword.js +3 -0
  94. package/built/api/translation-memory/params/search-keyword.js.map +1 -0
  95. package/built/api/translation-memory/params/source-type.d.ts +7 -0
  96. package/built/api/translation-memory/params/source-type.js +12 -0
  97. package/built/api/translation-memory/params/source-type.js.map +1 -0
  98. package/built/api/translation-memory/params/string-search-tags.d.ts +6 -0
  99. package/built/api/translation-memory/params/string-search-tags.js +3 -0
  100. package/built/api/translation-memory/params/string-search-tags.js.map +1 -0
  101. package/built/api/translation-memory/params/translation-quality-type.d.ts +5 -0
  102. package/built/api/translation-memory/params/translation-quality-type.js +10 -0
  103. package/built/api/translation-memory/params/translation-quality-type.js.map +1 -0
  104. package/built/api/translation-memory/params/tu-translation-quality-type.d.ts +6 -0
  105. package/built/api/translation-memory/params/tu-translation-quality-type.js +11 -0
  106. package/built/api/translation-memory/params/tu-translation-quality-type.js.map +1 -0
  107. package/built/api/translation-memory/params/update-tm-parameters.d.ts +6 -0
  108. package/built/api/translation-memory/params/update-tm-parameters.js +20 -0
  109. package/built/api/translation-memory/params/update-tm-parameters.js.map +1 -0
  110. package/built/index.d.ts +27 -0
  111. package/built/index.js +27 -0
  112. package/built/index.js.map +1 -1
  113. package/built/package.json +1 -1
  114. package/built/test/integration/translation-memory.spec.d.ts +1 -0
  115. package/built/test/integration/translation-memory.spec.js +138 -0
  116. package/built/test/integration/translation-memory.spec.js.map +1 -0
  117. package/built/test/translation-memory.spec.d.ts +1 -0
  118. package/built/test/translation-memory.spec.js +712 -0
  119. package/built/test/translation-memory.spec.js.map +1 -0
  120. package/index.ts +27 -0
  121. package/package.json +1 -1
  122. package/test/data/sample.tmx +12 -0
  123. package/test/integration/translation-memory.spec.ts +182 -0
  124. package/test/translation-memory.spec.ts +868 -0
@@ -0,0 +1,868 @@
1
+ import * as fs from "fs";
2
+ import sinon from "sinon";
3
+ import assert from "assert";
4
+ import { authMock, loggerMock, responseMock } from "./mock";
5
+ import { SmartlingTranslationMemoryApi } from "../api/translation-memory/index";
6
+ import { SmartlingAuthApi } from "../api/auth";
7
+ import { ImportTmxFileParameters } from "../api/translation-memory/params/import-tmx-file-parameters";
8
+ import { EscapeSmpUnicodeParserDirective } from "../api/translation-memory/params/escape-smp-unicode-parser-directive";
9
+ import { CreateStringSearchFilterParameters } from "../api/translation-memory/params/create-string-search-filter-parameters";
10
+ import { BulkExportTmStringsParameters } from "../api/translation-memory/params/bulk-export-tm-strings-parameters";
11
+ import { ExtendedExportMetadataType } from "../api/translation-memory/params/extended-export-metadata-type";
12
+ import { CreateTmParameters } from "../api/translation-memory/params/create-tm-parameters";
13
+ import { UpdateTmParameters } from "../api/translation-memory/params/update-tm-parameters";
14
+ import { TranslationQualityType } from "../api/translation-memory/params/translation-quality-type";
15
+ import { LeverageType } from "../api/translation-memory/params/leverage-type";
16
+ import { SourceType } from "../api/translation-memory/params/source-type";
17
+ import { TuTranslationQualityType } from "../api/translation-memory/params/tu-translation-quality-type";
18
+ import { KeywordSearchType } from "../api/translation-memory/params/keyword-search-type";
19
+ import { SmartlingException } from "../api/exception/index";
20
+ import { streamToString } from "./stream-to-string";
21
+
22
+ function formDataText(formData: { _streams: Array<unknown> }): string {
23
+ // eslint-disable-next-line no-underscore-dangle
24
+ return formData._streams.filter((entry) => typeof entry === "string").join("");
25
+ }
26
+
27
+ const noRequestIdHeaders = { get: (): string | null => null };
28
+
29
+ function tmGraphQlResponseMock(operationName: string, tm: Record<string, unknown>): {
30
+ status: number;
31
+ headers: { get: (name: string) => string | null };
32
+ text: () => Promise<string>;
33
+ } {
34
+ return {
35
+ status: 200,
36
+ headers: noRequestIdHeaders,
37
+ text: async (): Promise<string> => JSON.stringify({
38
+ data: {
39
+ [operationName]: tm
40
+ }
41
+ })
42
+ };
43
+ }
44
+
45
+ const sampleTm = {
46
+ tmUid: "j45v1uvsi3tx",
47
+ name: "Integration test TM",
48
+ description: "Created by the SDK test suite",
49
+ sourceLocaleId: "en-US",
50
+ sourceLocale: {
51
+ localeId: "en-US",
52
+ localeDescription: "English (United States)",
53
+ rtlLanguage: false,
54
+ wordSpacing: true
55
+ },
56
+ creator: { userUid: "creatorUid", name: "Creator Name" },
57
+ created: "2026-06-17T00:09:44Z",
58
+ lastModifier: { userUid: "modifierUid", name: "Modifier Name" },
59
+ lastModified: "2026-06-17T00:09:44Z",
60
+ localeSummary: [],
61
+ translationQuality: "TRANSLATION",
62
+ managedWordsEnabled: false
63
+ };
64
+
65
+ describe("SmartlingTranslationMemoryApi class tests.", () => {
66
+ const accountUid = "testAccountUid";
67
+ let translationMemoryApi: SmartlingTranslationMemoryApi;
68
+ let fetchStub;
69
+ let uaStub;
70
+ let responseMockJsonStub;
71
+
72
+ beforeEach(() => {
73
+ translationMemoryApi = new SmartlingTranslationMemoryApi(
74
+ "https://test.com", authMock as unknown as SmartlingAuthApi, loggerMock
75
+ );
76
+ fetchStub = sinon.stub(translationMemoryApi, "fetch");
77
+ uaStub = sinon.stub(translationMemoryApi, "ua");
78
+ responseMockJsonStub = sinon.stub(responseMock, "json");
79
+
80
+ uaStub.returns("test_user_agent");
81
+ fetchStub.returns(responseMock);
82
+ responseMockJsonStub.returns({ response: {} });
83
+ });
84
+
85
+ afterEach(() => {
86
+ fetchStub.restore();
87
+ responseMockJsonStub.restore();
88
+ uaStub.restore();
89
+ });
90
+
91
+ describe("Methods", () => {
92
+ it("Import TMX file", async () => {
93
+ const params = new ImportTmxFileParameters()
94
+ .setFileFromLocalFilePath("./test/data/file.xml")
95
+ .setFileUri("test/file.tmx")
96
+ .setTmUid("j45v1uvsi3tx")
97
+ .setDescription("Integration test import")
98
+ .setTranslatorNameLocation("@tu.changeid")
99
+ .setTranslationDateLocation("@tu.creationdate")
100
+ .setVariantLocation("@prop.type.x-SID:SingleString")
101
+ .setFileUriLocation("@prop.type.x-idiom-source-ipath")
102
+ .setEscapeSmpUnicodeParserDirective(EscapeSmpUnicodeParserDirective.ON);
103
+
104
+ await translationMemoryApi.importTmxFile(accountUid, params);
105
+
106
+ sinon.assert.calledOnce(fetchStub);
107
+
108
+ assert.equal(
109
+ fetchStub.getCall(0).args[0],
110
+ `https://test.com/tm-tmx-api/v2/accounts/${accountUid}/tmx/file`
111
+ );
112
+ assert.equal(fetchStub.getCall(0).args[1].method, "post");
113
+ assert.equal(
114
+ fetchStub.getCall(0).args[1].headers.Authorization,
115
+ "test_token_type test_access_token"
116
+ );
117
+
118
+ assert.equal(
119
+ // eslint-disable-next-line no-underscore-dangle
120
+ await streamToString(fetchStub.getCall(0).args[1].body._streams[1]),
121
+ fs.readFileSync(fs.realpathSync("./test/data/file.xml"), "utf8")
122
+ );
123
+
124
+ const fields = formDataText(fetchStub.getCall(0).args[1].body);
125
+
126
+ assert.ok(fields.includes("name=\"file\""));
127
+ assert.ok(fields.includes("name=\"fileUri\"") && fields.includes("test/file.tmx"));
128
+ assert.ok(fields.includes("name=\"tmUid\"") && fields.includes("j45v1uvsi3tx"));
129
+ assert.ok(fields.includes("name=\"description\"") && fields.includes("Integration test import"));
130
+ assert.ok(fields.includes("name=\"translatorNameLocation\"") && fields.includes("@tu.changeid"));
131
+ assert.ok(fields.includes("name=\"translationDateLocation\"") && fields.includes("@tu.creationdate"));
132
+ assert.ok(fields.includes("name=\"variantLocation\"") && fields.includes("@prop.type.x-SID:SingleString"));
133
+ assert.ok(fields.includes("name=\"fileUriLocation\"") && fields.includes("@prop.type.x-idiom-source-ipath"));
134
+ assert.ok(fields.includes("name=\"escapeSmpUnicodeParserDirective\"") && fields.includes("on"));
135
+ });
136
+
137
+ it("Import TMX file: as string", async () => {
138
+ const params = new ImportTmxFileParameters()
139
+ .setFileContent(fs.readFileSync(fs.realpathSync("./test/data/file.xml"), "utf8"))
140
+ .setFileUri("test/file.tmx")
141
+ .setTmUid("j45v1uvsi3tx");
142
+
143
+ await translationMemoryApi.importTmxFile(accountUid, params);
144
+
145
+ sinon.assert.calledOnce(fetchStub);
146
+ assert.equal(
147
+ // eslint-disable-next-line no-underscore-dangle
148
+ await streamToString(fetchStub.getCall(0).args[1].body._streams[1]),
149
+ fs.readFileSync(fs.realpathSync("./test/data/file.xml"), "utf8")
150
+ );
151
+ });
152
+
153
+ it("Import TMX file: from buffer", async () => {
154
+ const fileBuffer = fs.readFileSync(fs.realpathSync("./test/data/file.xml"));
155
+ const params = new ImportTmxFileParameters()
156
+ .setFileContentFromBuffer(fileBuffer)
157
+ .setFileUri("test/file.tmx")
158
+ .setTmUid("j45v1uvsi3tx");
159
+
160
+ await translationMemoryApi.importTmxFile(accountUid, params);
161
+
162
+ sinon.assert.calledOnce(fetchStub);
163
+ assert.ok(
164
+ // eslint-disable-next-line no-underscore-dangle
165
+ fetchStub.getCall(0).args[1].body._streams[1].equals(fileBuffer)
166
+ );
167
+ });
168
+
169
+ it("Import TMX file: FormData body is not reconverted on a 401 retry", async () => {
170
+ const params = new ImportTmxFileParameters()
171
+ .setFileFromLocalFilePath("./test/data/file.xml")
172
+ .setFileUri("test/file.tmx")
173
+ .setTmUid("j45v1uvsi3tx");
174
+
175
+ fetchStub.onCall(0).returns({ status: 401 });
176
+ fetchStub.onCall(1).returns(responseMock);
177
+
178
+ await translationMemoryApi.importTmxFile(accountUid, params);
179
+
180
+ sinon.assert.calledTwice(fetchStub);
181
+
182
+ // alterRequestData() runs again on the retry with the same opts object - the guard
183
+ // must recognize the body is already a FormData instance and leave it untouched,
184
+ // rather than re-running Object.keys()/forEach() over FormData's own internals.
185
+ assert.strictEqual(
186
+ fetchStub.getCall(0).args[1].body,
187
+ fetchStub.getCall(1).args[1].body
188
+ );
189
+
190
+ assert.equal(
191
+ // eslint-disable-next-line no-underscore-dangle
192
+ await streamToString(fetchStub.getCall(1).args[1].body._streams[1]),
193
+ fs.readFileSync(fs.realpathSync("./test/data/file.xml"), "utf8")
194
+ );
195
+ });
196
+
197
+ it("Import TMX file: warns when the response has partial import errors", async () => {
198
+ const warnStub = sinon.stub(loggerMock, "warn");
199
+
200
+ fetchStub.returns({
201
+ status: 200,
202
+ text: async (): Promise<string> => JSON.stringify({
203
+ response: {
204
+ data: {
205
+ message: null,
206
+ wordCount: 10,
207
+ stringCount: 5,
208
+ translationMemoryImportErrors: [
209
+ { messages: ["bad string"], line: 3, column: 1 }
210
+ ],
211
+ parsingErrors: null,
212
+ jobUid: "job-1",
213
+ tmxUid: "tmx-1"
214
+ }
215
+ }
216
+ })
217
+ });
218
+
219
+ const params = new ImportTmxFileParameters()
220
+ .setFileContentFromBuffer(Buffer.from("<tmx/>"))
221
+ .setFileUri("test/file.tmx")
222
+ .setTmUid("j45v1uvsi3tx");
223
+
224
+ const result = await translationMemoryApi.importTmxFile(accountUid, params);
225
+
226
+ assert.equal(result.jobUid, "job-1");
227
+ assert.equal(result.tmxUid, "tmx-1");
228
+ assert.equal(result.wordCount, 10);
229
+ assert.equal(result.stringCount, 5);
230
+ sinon.assert.calledOnce(warnStub);
231
+ assert.ok(warnStub.getCall(0).args[0].includes("bad string"));
232
+
233
+ warnStub.restore();
234
+ });
235
+
236
+ it("Import TMX file: does not warn when error arrays are merely empty", async () => {
237
+ const warnStub = sinon.stub(loggerMock, "warn");
238
+
239
+ fetchStub.returns({
240
+ status: 200,
241
+ text: async (): Promise<string> => JSON.stringify({
242
+ response: {
243
+ data: {
244
+ message: null,
245
+ wordCount: 10,
246
+ stringCount: 5,
247
+ translationMemoryImportErrors: [],
248
+ parsingErrors: [],
249
+ jobUid: "job-1",
250
+ tmxUid: "tmx-1"
251
+ }
252
+ }
253
+ })
254
+ });
255
+
256
+ const params = new ImportTmxFileParameters()
257
+ .setFileContentFromBuffer(Buffer.from("<tmx/>"))
258
+ .setFileUri("test/file.tmx")
259
+ .setTmUid("j45v1uvsi3tx");
260
+
261
+ await translationMemoryApi.importTmxFile(accountUid, params);
262
+
263
+ sinon.assert.notCalled(warnStub);
264
+
265
+ warnStub.restore();
266
+ });
267
+
268
+ it("Download TMX file", async () => {
269
+ const downloadResponseMock = {
270
+ status: 200,
271
+ headers: {
272
+ get: (name: string): string | null => {
273
+ if (name === "content-type") {
274
+ return "application/xml";
275
+ }
276
+
277
+ if (name === "content-disposition") {
278
+ return "attachment; filename=\"export.tmx\"";
279
+ }
280
+
281
+ return null;
282
+ }
283
+ },
284
+ arrayBuffer: async (): Promise<ArrayBuffer> => new ArrayBuffer(8)
285
+ };
286
+
287
+ fetchStub.returns(downloadResponseMock);
288
+
289
+ // A real tmxDownloadKey is base64 and can contain "+"/"/"/"=" - all of which must be
290
+ // percent-encoded or they'd be misread as URL path/query syntax.
291
+ const result = await translationMemoryApi.downloadTmxFile(accountUid, "a+b/c==");
292
+
293
+ sinon.assert.calledOnceWithExactly(
294
+ fetchStub,
295
+ `https://test.com/tm-tmx-api/v2/accounts/${accountUid}/tmx/file/a%2Bb%2Fc%3D%3D`,
296
+ {
297
+ method: "get",
298
+ headers: {
299
+ Authorization: "test_token_type test_access_token",
300
+ "Content-Type": "application/json",
301
+ "User-Agent": "test_user_agent"
302
+ }
303
+ }
304
+ );
305
+
306
+ assert.equal(result.fileName, "export.tmx");
307
+ assert.equal(result.contentType, "application/xml");
308
+ });
309
+
310
+ it("Create string search", async () => {
311
+ const graphQlResponseMock = {
312
+ status: 200,
313
+ headers: noRequestIdHeaders,
314
+ text: async (): Promise<string> => JSON.stringify({
315
+ data: {
316
+ createStringSearch: {
317
+ searchUid: "2z68632hdvw2",
318
+ stringCount: 10,
319
+ wordCount: 20,
320
+ duplicateStrings: [],
321
+ duplicateSearch: false,
322
+ actionUid: "d23eb702-7358-11ea-bc55-0242ac130003",
323
+ offlineReportUid: "6a2f41a3-c54c-fce8-32d2-0324e1c32e22"
324
+ }
325
+ }
326
+ })
327
+ };
328
+
329
+ fetchStub.returns(graphQlResponseMock);
330
+
331
+ const filter = new CreateStringSearchFilterParameters()
332
+ .setTmUids(["j45v1uvsi3tx"])
333
+ .setSourceLocaleIds(["en-US"]);
334
+
335
+ const result = await translationMemoryApi.createStringSearch(accountUid, filter);
336
+
337
+ sinon.assert.calledOnce(fetchStub);
338
+ assert.equal(
339
+ fetchStub.getCall(0).args[0],
340
+ `https://test.com/tm-graphql-api/v2/accounts/${accountUid}/graphql`
341
+ );
342
+
343
+ const sentBody = JSON.parse(fetchStub.getCall(0).args[1].body);
344
+ assert.deepEqual(sentBody.variables, {
345
+ createStringSearchFilter: {
346
+ tmUids: ["j45v1uvsi3tx"],
347
+ sourceLocaleIds: ["en-US"]
348
+ }
349
+ });
350
+ assert.ok(sentBody.query.includes("createStringSearch"));
351
+ [
352
+ "searchUid", "stringCount", "wordCount", "duplicateStrings", "duplicateKey",
353
+ "count", "srcText", "srcLocale", "translationLocale", "duplicateSearch",
354
+ "actionUid", "offlineReportUid"
355
+ ].forEach((field) => {
356
+ assert.ok(sentBody.query.includes(field), `query is missing field "${field}"`);
357
+ });
358
+
359
+ assert.equal(result.searchUid, "2z68632hdvw2");
360
+ assert.equal(result.stringCount, 10);
361
+ });
362
+
363
+ it("Create string search: GraphQL error is thrown", async () => {
364
+ const graphQlResponseMock = {
365
+ status: 200,
366
+ headers: noRequestIdHeaders,
367
+ text: async (): Promise<string> => JSON.stringify({
368
+ errors: [{ message: "tmUids is required" }]
369
+ })
370
+ };
371
+
372
+ fetchStub.returns(graphQlResponseMock);
373
+
374
+ await assert.rejects(
375
+ translationMemoryApi.createStringSearch(
376
+ accountUid, new CreateStringSearchFilterParameters()
377
+ ),
378
+ (e: SmartlingException) => {
379
+ assert.ok(e instanceof SmartlingException);
380
+ assert.ok(e.message.includes("tmUids is required"));
381
+ assert.deepEqual(
382
+ e.getPayload().errors,
383
+ [{ message: "tmUids is required" }]
384
+ );
385
+
386
+ return true;
387
+ }
388
+ );
389
+ });
390
+
391
+ it("Create string search: an empty errors array does not throw", async () => {
392
+ fetchStub.returns({
393
+ status: 200,
394
+ headers: noRequestIdHeaders,
395
+ text: async (): Promise<string> => JSON.stringify({
396
+ errors: [],
397
+ data: { createStringSearch: { searchUid: "2z68632hdvw2" } }
398
+ })
399
+ });
400
+
401
+ const result = await translationMemoryApi.createStringSearch(
402
+ accountUid, new CreateStringSearchFilterParameters()
403
+ );
404
+
405
+ assert.equal(result.searchUid, "2z68632hdvw2");
406
+ });
407
+
408
+ it("Create string search: a null result is returned as null, not thrown", async () => {
409
+ fetchStub.returns({
410
+ status: 200,
411
+ headers: noRequestIdHeaders,
412
+ text: async (): Promise<string> => JSON.stringify({
413
+ data: { createStringSearch: null }
414
+ })
415
+ });
416
+
417
+ const result = await translationMemoryApi.createStringSearch(
418
+ accountUid, new CreateStringSearchFilterParameters()
419
+ );
420
+
421
+ assert.equal(result, null);
422
+ });
423
+
424
+ it("Create string search: requestId is propagated into the thrown exception", async () => {
425
+ fetchStub.returns({
426
+ status: 200,
427
+ headers: { get: (name: string): string | null => (name === "x-sl-requestid" ? "test-request-id" : null) },
428
+ text: async (): Promise<string> => JSON.stringify({
429
+ errors: [{ message: "tmUids is required" }]
430
+ })
431
+ });
432
+
433
+ await assert.rejects(
434
+ translationMemoryApi.createStringSearch(
435
+ accountUid, new CreateStringSearchFilterParameters()
436
+ ),
437
+ (e: SmartlingException) => {
438
+ assert.equal(e.getPayload().requestId, "test-request-id");
439
+
440
+ return true;
441
+ }
442
+ );
443
+ });
444
+
445
+ it("Create string search: malformed JSON response throws", async () => {
446
+ fetchStub.returns({
447
+ status: 200,
448
+ headers: noRequestIdHeaders,
449
+ text: async (): Promise<string> => "not json"
450
+ });
451
+
452
+ await assert.rejects(
453
+ translationMemoryApi.createStringSearch(
454
+ accountUid, new CreateStringSearchFilterParameters()
455
+ ),
456
+ (e: SmartlingException) => {
457
+ assert.ok(e instanceof SmartlingException);
458
+ assert.ok(e.message.includes("Couldn't parse GraphQL response json"));
459
+ assert.ok(e.getNestedException() instanceof Error);
460
+
461
+ return true;
462
+ }
463
+ );
464
+ });
465
+
466
+ it("Create string search: response missing data entirely throws", async () => {
467
+ fetchStub.returns({
468
+ status: 200,
469
+ headers: noRequestIdHeaders,
470
+ text: async (): Promise<string> => JSON.stringify({})
471
+ });
472
+
473
+ await assert.rejects(
474
+ translationMemoryApi.createStringSearch(
475
+ accountUid, new CreateStringSearchFilterParameters()
476
+ ),
477
+ (e: SmartlingException) => {
478
+ assert.ok(e instanceof SmartlingException);
479
+ assert.ok(e.message.includes("is missing the expected field"));
480
+
481
+ return true;
482
+ }
483
+ );
484
+ });
485
+
486
+ it("Create string search: data present but missing the operation's own key throws", async () => {
487
+ // Distinct from a present-but-null value (handled by the "null result" test above) -
488
+ // this is the key itself being entirely absent, e.g. schema drift or a typo'd
489
+ // operationName, which should fail loudly rather than being coerced to null.
490
+ fetchStub.returns({
491
+ status: 200,
492
+ headers: noRequestIdHeaders,
493
+ text: async (): Promise<string> => JSON.stringify({
494
+ data: { someOtherOperation: { searchUid: "abc" } }
495
+ })
496
+ });
497
+
498
+ await assert.rejects(
499
+ translationMemoryApi.createStringSearch(
500
+ accountUid, new CreateStringSearchFilterParameters()
501
+ ),
502
+ (e: SmartlingException) => {
503
+ assert.ok(e instanceof SmartlingException);
504
+ assert.ok(e.message.includes("is missing the expected field"));
505
+
506
+ return true;
507
+ }
508
+ );
509
+ });
510
+
511
+ it("Create string search: a non-array errors field logs a warning and does not throw", async () => {
512
+ const warnStub = sinon.stub(loggerMock, "warn");
513
+
514
+ fetchStub.returns({
515
+ status: 200,
516
+ headers: noRequestIdHeaders,
517
+ text: async (): Promise<string> => JSON.stringify({
518
+ errors: { message: "unexpected shape" },
519
+ data: { createStringSearch: { searchUid: "2z68632hdvw2" } }
520
+ })
521
+ });
522
+
523
+ const result = await translationMemoryApi.createStringSearch(
524
+ accountUid, new CreateStringSearchFilterParameters()
525
+ );
526
+
527
+ assert.ok(result);
528
+ assert.equal(result.searchUid, "2z68632hdvw2");
529
+ sinon.assert.calledOnce(warnStub);
530
+ assert.ok(warnStub.getCall(0).args[0].includes("unexpected"));
531
+
532
+ warnStub.restore();
533
+ });
534
+
535
+ it("Bulk export TM strings", async () => {
536
+ const graphQlResponseMock = {
537
+ status: 200,
538
+ headers: noRequestIdHeaders,
539
+ text: async (): Promise<string> => JSON.stringify({
540
+ data: {
541
+ bulkExportTmStrings: {
542
+ actionUid: "d23eb702-7358-11ea-bc55-0242ac130003",
543
+ totalCount: 123,
544
+ processedCount: 0,
545
+ errorCount: 0,
546
+ executionComplete: false,
547
+ actionStatus: "QUEUEING"
548
+ }
549
+ }
550
+ })
551
+ };
552
+
553
+ fetchStub.returns(graphQlResponseMock);
554
+
555
+ const params = new BulkExportTmStringsParameters()
556
+ .setSearchUid("2z68632hdvw2")
557
+ .setExtendedMetadata(true)
558
+ .setExtendedMetadataType(ExtendedExportMetadataType.FIRST_TRANSLATED_STRINGS)
559
+ .setIncludeAllVariants(true);
560
+
561
+ const result = await translationMemoryApi.bulkExportTmStrings(accountUid, params);
562
+
563
+ const sentBody = JSON.parse(fetchStub.getCall(0).args[1].body);
564
+ assert.deepEqual(sentBody.variables, {
565
+ bulkExportTmStrings: {
566
+ searchUid: "2z68632hdvw2",
567
+ extendedMetadata: true,
568
+ extendedMetadataType: "FIRST_TRANSLATED_STRINGS",
569
+ includeAllVariants: true
570
+ }
571
+ });
572
+ assert.ok(sentBody.query.includes("bulkExportTmStrings"));
573
+ assert.ok(sentBody.query.includes("executionComplete"));
574
+ assert.ok(sentBody.query.includes("actionStatus"));
575
+
576
+ assert.ok(result);
577
+ assert.equal(result.actionUid, "d23eb702-7358-11ea-bc55-0242ac130003");
578
+ assert.equal(result.actionStatus, "QUEUEING");
579
+ });
580
+
581
+ it("Get bulk export TM strings status", async () => {
582
+ const debugStub = sinon.stub(loggerMock, "debug");
583
+
584
+ const graphQlResponseMock = {
585
+ status: 200,
586
+ headers: noRequestIdHeaders,
587
+ text: async (): Promise<string> => JSON.stringify({
588
+ data: {
589
+ bulkExportTmStringsStatus: {
590
+ actionUid: "d23eb702-7358-11ea-bc55-0242ac130003",
591
+ totalCount: 123,
592
+ processedCount: 123,
593
+ errorCount: 0,
594
+ executionComplete: true,
595
+ tmxDownloadKey: "ZGpoaDczaGRqc2o6MA==",
596
+ expired: false,
597
+ actionStatus: "SUCCESS"
598
+ }
599
+ }
600
+ })
601
+ };
602
+
603
+ fetchStub.returns(graphQlResponseMock);
604
+
605
+ const result = await translationMemoryApi.getBulkExportTmStringsStatus(
606
+ accountUid, "d23eb702-7358-11ea-bc55-0242ac130003"
607
+ );
608
+
609
+ const sentBody = JSON.parse(fetchStub.getCall(0).args[1].body);
610
+ assert.deepEqual(sentBody.variables, {
611
+ actionUid: "d23eb702-7358-11ea-bc55-0242ac130003"
612
+ });
613
+ assert.ok(sentBody.query.includes("bulkExportTmStringsStatus"));
614
+ assert.ok(sentBody.query.includes("tmxDownloadKey"));
615
+ assert.ok(sentBody.query.includes("expired"));
616
+
617
+ assert.ok(result);
618
+ assert.equal(result.tmxDownloadKey, "ZGpoaDczaGRqc2o6MA==");
619
+ assert.equal(result.executionComplete, true);
620
+
621
+ // tmxDownloadKey is a bearer-style download token - it must never reach the debug log
622
+ // verbatim, only redacted.
623
+ sinon.assert.calledOnce(debugStub);
624
+ const loggedText = debugStub.getCall(0).args[0];
625
+
626
+ assert.ok(loggedText.includes("[REDACTED]"));
627
+ assert.ok(!loggedText.includes("ZGpoaDczaGRqc2o6MA=="));
628
+
629
+ debugStub.restore();
630
+ });
631
+
632
+ it("Get bulk export TM strings status: tmxDownloadKey is redacted in a thrown exception's payload too", async () => {
633
+ // A field-level error can accompany otherwise-populated partial data (see the
634
+ // "GraphQL error is thrown" test) - the live download token must not leak through
635
+ // that path either, not just the debug log.
636
+ fetchStub.returns({
637
+ status: 200,
638
+ headers: noRequestIdHeaders,
639
+ text: async (): Promise<string> => JSON.stringify({
640
+ errors: [{ message: "partial failure" }],
641
+ data: {
642
+ bulkExportTmStringsStatus: {
643
+ actionUid: "d23eb702-7358-11ea-bc55-0242ac130003",
644
+ tmxDownloadKey: "ZGpoaDczaGRqc2o6MA=="
645
+ }
646
+ }
647
+ })
648
+ });
649
+
650
+ await assert.rejects(
651
+ translationMemoryApi.getBulkExportTmStringsStatus(
652
+ accountUid, "d23eb702-7358-11ea-bc55-0242ac130003"
653
+ ),
654
+ (e: SmartlingException) => {
655
+ const payloadText = JSON.stringify(e.getPayload());
656
+
657
+ assert.ok(payloadText.includes("[REDACTED]"));
658
+ assert.ok(!payloadText.includes("ZGpoaDczaGRqc2o6MA=="));
659
+
660
+ return true;
661
+ }
662
+ );
663
+ });
664
+
665
+ it("Create TM", async () => {
666
+ fetchStub.returns(tmGraphQlResponseMock("createTm", sampleTm));
667
+
668
+ const params = new CreateTmParameters()
669
+ .setName("Integration test TM")
670
+ .setDescription("Created by the SDK test suite")
671
+ .setSourceLocaleId("en-US")
672
+ .setTranslationQuality(TranslationQualityType.TRANSLATION);
673
+
674
+ const result = await translationMemoryApi.createTm(accountUid, params);
675
+
676
+ sinon.assert.calledOnce(fetchStub);
677
+ assert.equal(
678
+ fetchStub.getCall(0).args[0],
679
+ `https://test.com/tm-graphql-api/v2/accounts/${accountUid}/graphql`
680
+ );
681
+
682
+ const sentBody = JSON.parse(fetchStub.getCall(0).args[1].body);
683
+ assert.deepEqual(sentBody.variables, {
684
+ createTm: {
685
+ name: "Integration test TM",
686
+ description: "Created by the SDK test suite",
687
+ sourceLocaleId: "en-US",
688
+ translationQuality: "TRANSLATION"
689
+ }
690
+ });
691
+ // Every field this domain's TmDto declares must actually be requested by TM_FIELDS -
692
+ // a typo in either one would silently desync the query from the DTO it populates.
693
+ assert.ok(sentBody.query.includes("createTm"));
694
+ [
695
+ "tmUid", "name", "description", "sourceLocaleId", "sourceLocale", "localeId",
696
+ "localeDescription", "rtlLanguage", "wordSpacing", "creator", "userUid", "created",
697
+ "lastModifier", "lastModified", "localeSummary", "translationLocaleId",
698
+ "translationLocale", "stringCount", "wordCount", "translationQuality",
699
+ "managedWordsEnabled"
700
+ ].forEach((field) => {
701
+ assert.ok(sentBody.query.includes(field), `query is missing field "${field}"`);
702
+ });
703
+
704
+ assert.ok(result);
705
+ assert.equal(result.tmUid, "j45v1uvsi3tx");
706
+ assert.ok(result.created instanceof Date);
707
+ assert.ok(result.lastModified instanceof Date);
708
+ });
709
+
710
+ it("Create TM: a null result is returned as null, not thrown", async () => {
711
+ fetchStub.returns({
712
+ status: 200,
713
+ headers: noRequestIdHeaders,
714
+ text: async (): Promise<string> => JSON.stringify({ data: { createTm: null } })
715
+ });
716
+
717
+ const result = await translationMemoryApi.createTm(
718
+ accountUid, new CreateTmParameters()
719
+ );
720
+
721
+ assert.equal(result, null);
722
+ });
723
+
724
+ it("Update TM", async () => {
725
+ fetchStub.returns(tmGraphQlResponseMock("updateTm", sampleTm));
726
+
727
+ const params = new UpdateTmParameters()
728
+ .setTmUid("j45v1uvsi3tx")
729
+ .setName("Renamed TM")
730
+ .setDescription("Updated description");
731
+
732
+ const result = await translationMemoryApi.updateTm(accountUid, params);
733
+
734
+ const sentBody = JSON.parse(fetchStub.getCall(0).args[1].body);
735
+ assert.deepEqual(sentBody.variables, {
736
+ updateTm: {
737
+ tmUid: "j45v1uvsi3tx",
738
+ name: "Renamed TM",
739
+ description: "Updated description"
740
+ }
741
+ });
742
+
743
+ assert.ok(result);
744
+ assert.equal(result.tmUid, "j45v1uvsi3tx");
745
+ });
746
+
747
+ it("Delete TM", async () => {
748
+ fetchStub.returns(tmGraphQlResponseMock("deleteTm", sampleTm));
749
+
750
+ const result = await translationMemoryApi.deleteTm(accountUid, "j45v1uvsi3tx");
751
+
752
+ const sentBody = JSON.parse(fetchStub.getCall(0).args[1].body);
753
+ assert.deepEqual(sentBody.variables, {
754
+ deleteTm: { tmUid: "j45v1uvsi3tx" }
755
+ });
756
+
757
+ assert.ok(result);
758
+ assert.equal(result.tmUid, "j45v1uvsi3tx");
759
+ });
760
+
761
+ it("Create string search: exercises every CreateStringSearchFilterParameters setter", () => {
762
+ const translationDateRangeStart = new Date("2026-01-01T00:00:00Z");
763
+ const translationDateRangeEnd = new Date("2026-02-01T00:00:00Z");
764
+ const editDateRangeStart = new Date("2026-01-02T00:00:00Z");
765
+ const editDateRangeEnd = new Date("2026-02-02T00:00:00Z");
766
+ const reviewDateRangeStart = new Date("2026-01-03T00:00:00Z");
767
+ const reviewDateRangeEnd = new Date("2026-02-03T00:00:00Z");
768
+ const authorizationDateRangeStart = new Date("2026-01-04T00:00:00Z");
769
+ const authorizationDateRangeEnd = new Date("2026-02-04T00:00:00Z");
770
+ const ingestedDateRangeStart = new Date("2026-01-05T00:00:00Z");
771
+ const ingestedDateRangeEnd = new Date("2026-02-05T00:00:00Z");
772
+ const lastTranslatedAfter = new Date("2026-01-06T00:00:00Z");
773
+ const lastTranslatedBefore = new Date("2026-02-06T00:00:00Z");
774
+ const firstTranslatedAfter = new Date("2026-01-07T00:00:00Z");
775
+ const firstTranslatedBefore = new Date("2026-02-07T00:00:00Z");
776
+ const keyword = { searchText: "year", caseSensitive: true, entireStringMatch: false, searchType: KeywordSearchType.BASIC };
777
+ const sourceKeyword = { searchText: "src", searchType: KeywordSearchType.EXACT_MATCH };
778
+ const translationKeyword = { searchText: "tgt", searchType: KeywordSearchType.REGEX };
779
+ const tags = { tags: ["tag1"], matchAllTags: true, noTags: false };
780
+ const searchOptions = {
781
+ highlightMatches: false,
782
+ searchDuplicates: true,
783
+ translationEqSource: false,
784
+ requestedResultsCount: 500
785
+ };
786
+
787
+ const filter = new CreateStringSearchFilterParameters()
788
+ .setTmUids(["tm1"])
789
+ .setSourceLocaleIds(["en-US"])
790
+ .setTranslationLocaleIds(["fr-FR"])
791
+ .setKeyword(keyword)
792
+ .setSourceKeyword(sourceKeyword)
793
+ .setTranslationKeyword(translationKeyword)
794
+ .setLeverageType(LeverageType.PUBLISHED)
795
+ .setHashcodes(["hash1"])
796
+ .setSmtHashcodes(["smt1"])
797
+ .setTranslatorUids(["translator1"])
798
+ .setTranslationDateRange(translationDateRangeStart, translationDateRangeEnd)
799
+ .setEditorUids(["editor1"])
800
+ .setEditDateRange(editDateRangeStart, editDateRangeEnd)
801
+ .setReviewerUids(["reviewer1"])
802
+ .setReviewDateRange(reviewDateRangeStart, reviewDateRangeEnd)
803
+ .setAuthorizerUids(["authorizer1"])
804
+ .setAuthorizationDateRange(authorizationDateRangeStart, authorizationDateRangeEnd)
805
+ .setIngestedDateRange(ingestedDateRangeStart, ingestedDateRangeEnd)
806
+ .setVariants(["variant1"])
807
+ .setProjectUids(["project1"])
808
+ .setExternalStep(true)
809
+ .setUrls(["https://example.com"])
810
+ .setFileNames(["file1.xml"])
811
+ .setKeys(["key1"])
812
+ .setDomains(["domain1"])
813
+ .setTmxFileNames(["tmx1.tmx"])
814
+ .setSources([SourceType.TMX])
815
+ .setTranslationQualities([TuTranslationQualityType.HUMAN])
816
+ .setFirstTranslatorUids(["firstTranslator1"])
817
+ .setLastTranslatedRange(lastTranslatedAfter, lastTranslatedBefore)
818
+ .setFirstTranslatedRange(firstTranslatedAfter, firstTranslatedBefore)
819
+ .setTags(tags)
820
+ .setOfflineReportUids(["offlineReport1"])
821
+ .setSearchOptions(searchOptions);
822
+
823
+ assert.deepEqual(filter.export(), {
824
+ tmUids: ["tm1"],
825
+ sourceLocaleIds: ["en-US"],
826
+ translationLocaleIds: ["fr-FR"],
827
+ keyword,
828
+ sourceKeyword,
829
+ translationKeyword,
830
+ leverageType: "PUBLISHED",
831
+ hashcodes: ["hash1"],
832
+ smtHashcodes: ["smt1"],
833
+ translatorUids: ["translator1"],
834
+ translationDateRangeStart,
835
+ translationDateRangeEnd,
836
+ editorUids: ["editor1"],
837
+ editDateRangeStart,
838
+ editDateRangeEnd,
839
+ reviewerUids: ["reviewer1"],
840
+ reviewDateRangeStart,
841
+ reviewDateRangeEnd,
842
+ authorizerUids: ["authorizer1"],
843
+ authorizationDateRangeStart,
844
+ authorizationDateRangeEnd,
845
+ ingestedDateRangeStart,
846
+ ingestedDateRangeEnd,
847
+ variants: ["variant1"],
848
+ projectUids: ["project1"],
849
+ externalStep: true,
850
+ urls: ["https://example.com"],
851
+ fileNames: ["file1.xml"],
852
+ keys: ["key1"],
853
+ domains: ["domain1"],
854
+ tmxFileNames: ["tmx1.tmx"],
855
+ sources: ["TMX"],
856
+ translationQualities: ["HUMAN"],
857
+ firstTranslatorUids: ["firstTranslator1"],
858
+ lastTranslatedAfter,
859
+ lastTranslatedBefore,
860
+ firstTranslatedAfter,
861
+ firstTranslatedBefore,
862
+ tags,
863
+ offlineReportUids: ["offlineReport1"],
864
+ searchOptions
865
+ });
866
+ });
867
+ });
868
+ });