Skip to content

Commit 5428969

Browse files
authored
Script include/translation util (#1792)
* Create translationUtil.js his SI is a translationUtil to for dynamic language translation for example english to French This script include for language translation will invoke flow designer action and sublfow to do the dynamic language transaltion * Create README.md Script include is created as translationUtil for dynamic language translation. for example english to French This script include for language translation will invoke flow designer action and sublfow to complete the real time language transaltion for instance suppose group table is updated with new group record having english as description text that can't be translated using OOTB translation tables in such scenario this UTIL will be a saviour * Update README.md README file is updated with util description
1 parent 1b97e2d commit 5428969

File tree

2 files changed

+387
-0
lines changed

2 files changed

+387
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Script include is created as translationUtil for dynamic language translation. for example english to French
2+
3+
This script include for language translation will invoke flow designer action and sublfow to complete the real time language transaltion for instance suppose group table is updated with new group record having english as description text that can't be translated using OOTB translation tables in such scenario this UTIL will be a saviour
4+
5+
The properties referred in this translation util is DUMMY name that needs to be replaced with actual property name
6+
7+
You need to Identify the AI translator for your language and update accordingly.
8+
9+
More details....
10+
11+
This PR created for script include that contain TranslationUtil script and readme file that descrive 'How it server the purpose for Dynamic field translation using specific translator'
12+
13+
It will Fetches and calculates runtime limits for translation requests from system properties. Further it will dynamically retrieves the translation API key using getSubscriptionKey().
14+
15+
The scope of this utility ranges from multilingual support based on the user's preferred language to dynamic field translation through integration with Flows and Actions.
16+
17+
The scope of this PR & SI to provide a translationUtil, the custom flow action and subflow is not within the scope of this Util, if anyone wants to use it they need to create there own subflow that detect and translate the language by using this SI.
18+
19+
Details of Utils
20+
21+
TranslationUtils is a custom Script Include, created to manage dynamic text translation and language detection without depending on ServiceNow’s Out-of-the-box (OOTB) Translation plugin (like Dynamic Translation or Localization framework).
22+
How it Works?
23+
24+
It will look for Custom REST connections (via http_connection and api_key_credentials)
25+
Flow actions / subflows for actual translation and detection (global.detect_language, global.hhfksjhd__translate_text)
26+
Custom batching, size limits, and buffer logic to optimize translation requests and avoid API overflows.
27+
This SI will do the translation & detection of texts & give the translated data as JSON output
28+
HOPE THIS HELPS TO CLARIFY.
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
/*
2+
This SI is a translationUtil to for dynamic language translation for example english to French
3+
4+
This script include for language translation will invoke flow designer action and sublfow to complete the real time language transaltion
5+
6+
The properties referred in this translation util is DUMMY name that needs to be replaced with actual property name
7+
8+
Identify the right AI translator for language and Update
9+
*/
10+
var TranslationUtils = Class.create();
11+
TranslationUtils.prototype = {
12+
initialize: function() {},
13+
14+
getLimits: function() {
15+
16+
var charLimit = parseInt(gs.getProperty('mycompany.dynamic.translation.char.limit', 30720));//51200=50KB 30720 = 30KB
17+
var requestBufferSize = parseInt(gs.getProperty('mycompany.dynamic.translation.request.buffer', 100));
18+
charLimit = (charLimit >= requestBufferSize) ? (charLimit - requestBufferSize) : charLimit;
19+
return {
20+
'charLimit': charLimit,
21+
'arrayLimit': parseInt(gs.getProperty('mycompany.dynamic.translation.array.limit', 10)),
22+
'textBufferSize': parseInt(gs.getProperty('mycompany.dynamic.translation.text.buffer', 4))
23+
};
24+
},
25+
26+
27+
getSubscriptionKey: function() {
28+
29+
/* try {
30+
31+
var result = sn_fd.FlowAPI.getRunner().action('<call FD action to get subscription key>').inForeground().run();
32+
var get_key_outputs = result.getOutputs();
33+
var subscription_key = get_key_outputs.subscription_key;
34+
35+
return subscription_key;
36+
37+
} catch (ex) {
38+
var message = ex.getMessage();
39+
gs.error(message);
40+
41+
}*/
42+
try {
43+
44+
var alias = gs.getProperty('mycompany.alias.ia.translation.id');
45+
var subscription_key = '';
46+
47+
var gr_connection = new GlideRecord('http_connection');
48+
gr_connection.addEncodedQuery('active=true^connection_alias=' + alias);
49+
gr_connection.query();
50+
51+
if (gr_connection.next()) {
52+
var gr_api_key = new GlideRecord('api_key_credentials');
53+
gr_api_key.get(gr_connection.getValue('credential'));
54+
subscription_key = gr_api_key.api_key.getDecryptedValue();
55+
}
56+
57+
return subscription_key;
58+
59+
} catch (ex) {
60+
var message = ex.getMessage();
61+
gs.error(message);
62+
}
63+
64+
},
65+
66+
67+
getKBSize: function(text, limits) {
68+
var bytes = text.length;
69+
for (var i = text.length - 1; i >= 0; i--) {
70+
var code = text.charCodeAt(i);
71+
if (code > 0x7f && code <= 0x7ff)
72+
bytes++;
73+
else if (code > 0x7ff && code <= 0xffff)
74+
bytes += 2;
75+
if (code >= 0xDC00 && code <= 0xDFFF)
76+
i--;
77+
}
78+
var textBufferSize = limits.textBufferSize;
79+
return bytes + textBufferSize;
80+
},
81+
82+
getBytesData: function(texts, limits) {
83+
var bytesData = {};
84+
for (var i = 0; i < texts.length; i++) {
85+
var kbSize = this.getKBSize(texts[i], limits);
86+
bytesData[texts[i]] = kbSize;
87+
}
88+
return bytesData;
89+
},
90+
91+
classifyBulkTexts: function(texts, charLimit, bytesData) {
92+
var count = 0;
93+
var classifiedData = {};
94+
classifiedData["smallTexts"] = [];
95+
classifiedData["largeTexts"] = [];
96+
classifiedData["isBatchingRequired"] = false;
97+
for (var i = 0; i < texts.length; i++) {
98+
if (bytesData[texts[i]] > charLimit) {
99+
classifiedData["largeTexts"].push(texts[i]);
100+
} else {
101+
classifiedData["smallTexts"].push(texts[i]);
102+
count = count + bytesData[texts[i]];
103+
if (count > charLimit) {
104+
classifiedData["isBatchingRequired"] = true;
105+
}
106+
}
107+
}
108+
return classifiedData;
109+
},
110+
111+
splitLargeTexts: function(texts, charLimit) {
112+
113+
var text_obj = {}; //text_obj = texts[0];
114+
var text_transl = []; //text_transl = text_obj["texts_to_translate"];
115+
var text_splited = [];
116+
//gs.log("charLimit : " + charLimit +'\n\ntext_transl : ' + JSON.stringify(text_transl), "BELLTR");
117+
//var text_array = texts[0].texts_to_translate;
118+
//for (var i = 0; i < text_transl.length; i++) {
119+
//var txt = text_transl[i];
120+
var txt = texts
121+
var kbSize = this.getKBSize(txt, limits);
122+
gs.log('kbSize : ' + kbSize +'\n\ntxt : ' + JSON.stringify(txt), "TELCOTR");
123+
if (kbSize > charLimit) {
124+
var text = txt
125+
var size = 0;
126+
var stop = 0
127+
gs.log('text : ' + text, "TELCOTR");
128+
while (size > charLimit && stop < 4) {
129+
text = txt.subString(0, charLimit);
130+
txt = txt.subString(charLimit);
131+
text_splited.push(text);
132+
gs.log('text_splited : ' + text_splited, "TELCOTR");
133+
size = this.getKBSize(txt, limits)
134+
stop++;
135+
}
136+
text_splited.push(txt)
137+
}
138+
139+
140+
return text_splited;
141+
},
142+
143+
144+
addLargeTextsToArray: function(result, largeTexts, targetLanguages) {
145+
146+
for (var large = 0; large < largeTexts.length; large++) {
147+
this.transformBatchTextsResponse([largeTexts[large]], targetLanguages, result);
148+
}
149+
},
150+
151+
getSortedBytesMap: function(texts, bytesData) {
152+
var bytesList = [];
153+
for (var i = 0; i < texts.length; i++) {
154+
var kbSize = bytesData[texts[i]];
155+
if (kbSize) {
156+
bytesList.push([kbSize, texts[i]]);
157+
}
158+
}
159+
return bytesList.sort();
160+
},
161+
162+
getBatchTexts: function(texts, bytesData, limits) {
163+
var bytesList = this.getSortedBytesMap(texts, bytesData);
164+
var weight = limits.charLimit;
165+
var splitTexts = [];
166+
var startIdx = 0;
167+
var arrayLength = bytesList.length;
168+
var endIdx = arrayLength - 1;
169+
var textsProcessed = 0;
170+
while (textsProcessed < arrayLength) {
171+
var singleSplit = [];
172+
var tempWeight = 0;
173+
while (singleSplit.length != limits.arrayLimit &&
174+
endIdx >= 0 &&
175+
startIdx <= endIdx &&
176+
weight >= (tempWeight + bytesList[endIdx][0])) {
177+
tempWeight += bytesList[endIdx][0];
178+
singleSplit.push(bytesList[endIdx][1]);
179+
endIdx -= 1;
180+
textsProcessed += 1;
181+
}
182+
while (singleSplit.length != limits.arrayLimit &&
183+
startIdx < arrayLength &&
184+
startIdx <= endIdx &&
185+
weight >= (tempWeight + bytesList[startIdx][0])) {
186+
tempWeight += bytesList[startIdx][0];
187+
singleSplit.push(bytesList[startIdx][1]);
188+
startIdx += 1;
189+
textsProcessed += 1;
190+
}
191+
splitTexts.push(singleSplit);
192+
}
193+
return splitTexts;
194+
},
195+
196+
197+
transformBatchTextsResponse: function(texts, targetLanguages, result) {
198+
for (var i = 0; i < targetLanguages.length; i++) {
199+
result.push({
200+
"texts_to_translate": texts,
201+
"target_language": targetLanguages[i]
202+
});
203+
}
204+
},
205+
206+
addSmallTextsToArray: function(smallTexts, result, limits, targetLanguages) {
207+
var tempSmallTexts = [];
208+
for (var i = 0; i < smallTexts.length; i++) {
209+
tempSmallTexts.push(smallTexts[i]);
210+
if (tempSmallTexts.length == limits.arrayLimit) {
211+
this.transformBatchTextsResponse(tempSmallTexts, targetLanguages, result);
212+
tempSmallTexts = [];
213+
}
214+
}
215+
if (tempSmallTexts.length > 0) {
216+
this.transformBatchTextsResponse(tempSmallTexts, targetLanguages, result);
217+
}
218+
219+
},
220+
221+
splitInputTextsIntoBatches: function(texts, limits, targetLanguages) {
222+
var result = [];
223+
var bytesData = this.getBytesData(texts, limits);
224+
var classifiedData = this.classifyBulkTexts(texts, limits.charLimit, bytesData);
225+
if (classifiedData["isBatchingRequired"]) {
226+
var splitTexts = this.getBatchTexts(classifiedData.smallTexts, bytesData, limits);
227+
228+
for (var idx = 0; idx < splitTexts.length; idx++) {
229+
var selectedTexts = splitTexts[idx];
230+
this.transformBatchTextsResponse(selectedTexts, targetLanguages, result);
231+
}
232+
} else {
233+
this.addSmallTextsToArray(classifiedData.smallTexts, result, limits, targetLanguages);
234+
}
235+
this.addLargeTextsToArray(result, classifiedData.largeTexts, targetLanguages);
236+
return result;
237+
},
238+
239+
240+
batchBulkTexts: function(texts, targetLanguages) {
241+
var limits = this.getLimits();
242+
//var test = this.splitLargeTexts(texts, limits.charLimit)
243+
//gs.log('test: ' + JSON.stringify(test), "TELCOTR")
244+
return this.splitInputTextsIntoBatches(texts, limits, targetLanguages);
245+
},
246+
247+
_getProcessedTextResult: function(textResult) {
248+
var processedTextResult = {};
249+
var textResultKeys = Object.keys(textResult);
250+
for (var keyIdx = 0; keyIdx < textResultKeys.length; keyIdx++) {
251+
var key = textResultKeys[keyIdx];
252+
if (key == 'text_translations') {
253+
var textTranslations = [];
254+
var languages = Object.keys(textResult.text_translations);
255+
for (var idx = 0; idx < languages.length; idx++) {
256+
var language = languages[idx];
257+
var textTranslation = {
258+
'translated_text': textResult.text_translations[language],
259+
'target_language': language
260+
};
261+
textTranslations.push(textTranslation);
262+
}
263+
processedTextResult.text_translations = textTranslations;
264+
} else {
265+
processedTextResult[key] = textResult[key];
266+
}
267+
}
268+
return processedTextResult;
269+
},
270+
271+
rearrangeJSONResult: function(texts, result, isTranslation) {
272+
var response = {
273+
'status': 'Success'
274+
};
275+
var rearrangedResponse = [];
276+
for (var i = 0; i < texts.length; i++) {
277+
var eachTextResult = result[texts[i]];
278+
if ('Error' === eachTextResult.status) {
279+
response['status'] = 'Error';
280+
}
281+
var processedTextResult = this._getProcessedTextResult(eachTextResult);
282+
rearrangedResponse.push(processedTextResult);
283+
}
284+
if (isTranslation)
285+
response['translations'] = rearrangedResponse;
286+
else
287+
response['detections'] = rearrangedResponse;
288+
return response;
289+
},
290+
291+
292+
detectLanguage: function(texts) {
293+
294+
try {
295+
var inputs = {};
296+
inputs['texts'] = texts; // Array.String
297+
298+
// Start Asynchronously: Uncomment to run in background. Code snippet will not have access to outputs.
299+
// sn_fd.FlowAPI.getRunner().subflow('<detect langugage FD Action>').inBackground().withInputs(inputs).run();
300+
301+
// Execute Synchronously: Run in foreground. Code snippet has access to outputs.
302+
var result = sn_fd.FlowAPI.getRunner().subflow('<detect langugage FD Action>').inForeground().withInputs(inputs).run();
303+
var outputs = result.getOutputs();
304+
305+
// Get Outputs:
306+
// Note: outputs can only be retrieved when executing synchronously.
307+
var detections = outputs['detections']; // Array.Object
308+
var status = outputs['status']; // Choice
309+
var rest_calls = outputs['rest_calls']; // Integer
310+
311+
return detections;
312+
313+
} catch (ex) {
314+
var message = ex.getMessage();
315+
gs.log(message, "AI Translator - detectLanguage");
316+
}
317+
318+
return '';
319+
320+
},
321+
322+
translateFields: function(texts, source_language, target_languages, additional_parameters) {
323+
324+
try {
325+
326+
var inputs = {};
327+
inputs['texts'] = texts; // Array.String
328+
inputs['source_language'] = source_language; // String
329+
inputs['target_languages'] = target_languages; // Array.String
330+
inputs['additional_parameters'] = additional_parameters; // Array.Object
331+
332+
// Start Asynchronously: Uncomment to run in background. Code snippet will not have access to outputs.
333+
// sn_fd.FlowAPI.getRunner().subflow('<FD ACTION for Text Translation>').inBackground().withInputs(inputs).run();
334+
335+
// Execute Synchronously: Run in foreground. Code snippet has access to outputs.
336+
var result = sn_fd.FlowAPI.getRunner().subflow('global.<FD ACTION for Text Translation>').inForeground().withInputs(inputs).run();
337+
var outputs = result.getOutputs();
338+
339+
// Get Outputs:
340+
// Note: outputs can only be retrieved when executing synchronously.
341+
var translations = outputs['translations']; // Array.Object
342+
var status = outputs['status']; // Choice
343+
var rest_calls = outputs['rest_calls']; // Integer
344+
345+
return translations;
346+
347+
} catch (ex) {
348+
var message = ex.getMessage();
349+
gs.log(message, "
350+
AI Translator - translateFields");
351+
}
352+
353+
return '';
354+
355+
},
356+
357+
358+
type: 'TranslationUtils'
359+
};

0 commit comments

Comments
 (0)