· 6 years ago · Dec 08, 2019, 01:48 PM
1/* eslint-disable no-console */
2/*! ===============================================
3Copyright:
4 DeepL GmbH
5 Im Mediapark 8a
6 50670 Cologne
7 Germany
8 www.deepl.com
9*/
10
11// ===========================================================================================================================================================
12// ===========================================================================================================================================================
13/*global M*/
14
15import '../utils/PubSub.js'
16import '../utils/U.js'
17import '../utils/Tracker.js'
18import '../utils/DateTime.js'
19import '../utils/DeviceProps.js'
20import '../utils/RPC.js'
21import '../utils/jQuery.js'
22import '../deepl.js'
23
24import './LMT_UI_Utils.js'
25import './LMT_UI_FeedbackMessage.js'
26import './LMT_Utils.js'
27import './LMT_Backend.js'
28import './LMT_log.js'
29import './LMT_UI_MessageBox.js'
30import './LMT_UI_MessageBox2.js'
31import './LMT_UI_SystemNotification.js'
32import './LMT_UI_Tooltip.js'
33
34/**
35 * @module LMT_WebTranslator_MainFunctionality
36 */
37M.define('LMT_WebTranslator_MainFunctionality', ['RPC', 'U', 'PubSub', '$' ,'queryVars', 'dlPageState', 'dlRPC', 'dlTexts',
38 'DeviceProps', 'LMT_UI_FeedbackMessage', 'LMT_Utils', 'LMT_Backend', 'LMT_log',
39 'LMT_UI_Utils', 'LMT_UI_MessageBox', 'LMT_UI_MessageBox2', 'LMT_UI_SystemNotification', 'LMT_UI_Tooltip', 'dlStats', 'H2', 'dlAnalytics',
40 'dlPrivacy'],
41 function _M__LMT_WebTranslator(RPC, U, PubSub, $, queryVars, dlPageState, dlRPC, dlTexts,
42 DeviceProps, LMT_UI_FeedbackMessage, LMT_Utils, LMT_Backend, log,
43 LMT_UI_Utils, LMT_UI_MessageBox, LMT_UI_MessageBox2, LMT_UI_SystemNotification, LMT_UI_Tooltip, dlStats, H2, dlAnalytics,
44 dlPrivacy){
45
46return {
47 initTranslator: function _initTranslator(webTranslator){
48"use strict";
49const TAG = '[WebTranslator]';
50const dbg = false;
51
52/**
53 * A FeatureManager can be used to dynamically combine different functionalities (like e.g. Object-mixins) with defined dependencies.
54 */
55class FeatureManager{ // features
56 constructor(){
57 this._featureEntries = {};
58 this.dbg = false;
59 }
60 _getFeatureEntry(featureId){
61 return this._featureEntries[featureId] || (this._featureEntries[featureId] = {
62 id: featureId,
63 state: undefined,
64 onAvailable: undefined,
65 value: undefined,
66 doLazyResolve: undefined,
67 });
68 }
69 /* (async) Resolves with the requested feature values when all required features become available.
70 - Does NOT show an error if the features never get resolved.
71 - Does NOT trigger the instantiation of lazy features.
72 */
73 with(featureIds, callback = null){
74 return new Promise((resolve, _reject) => {
75 const featureEntries = (Array.isArray(featureIds) ? featureIds : [featureIds]).map(id => this._getFeatureEntry(id));
76 let resolved = false;
77 function tryToResolve(){
78 if (!resolved){
79 const featureValues = [], missingFeatureEntries = [];
80 featureEntries.forEach(featureEntry => {
81 if (featureEntry.state === 'resolved'){
82 featureValues.push(featureEntry.value);
83 }
84 else {
85 missingFeatureEntries.push(featureEntry);
86 }
87 });
88 if (!missingFeatureEntries.length){
89 resolved = true;
90 callback && callback(...featureValues);
91 resolve(featureValues);
92 }
93 return missingFeatureEntries;
94 }
95 }
96 const missingEntries = tryToResolve();
97 if (missingEntries.length){
98 missingEntries.forEach(featureEntry => (featureEntry.onAvailable || (featureEntry.onAvailable = U.createCallOnce())).push(tryToResolve));
99 }
100 });
101 }
102 /* (async) Resolves with the requested feature values when all required features become available.
103 - Does show an error if the features can't be resolved after a timeout.
104 - Does trigger the instantiation of lazy features.
105 - Todo: Raise an error if the dependencies can't be resolved for a long time (longer than the warning message, to allow recovery in a lagggy situation)
106 */
107 require(featureIds, callback = null){
108 return new Promise((resolve, _reject) => {
109 const featureEntries = (Array.isArray(featureIds) ? featureIds : [featureIds]).map(id => this._getFeatureEntry(id));
110 let resolved = false;
111 let failureTimer;
112 function tryToResolve(){
113 if (!resolved){
114 const featureValues = [], missingFeatureEntries = [];
115 featureEntries.forEach(featureEntry => {
116 if (featureEntry.state === 'resolved'){
117 featureValues.push(featureEntry.value);
118 }
119 else {
120 missingFeatureEntries.push(featureEntry);
121 }
122 });
123 if (missingFeatureEntries.length){
124 missingFeatureEntries.forEach(entry => entry.state = 'requested');
125 }
126 else {
127 resolved = true;
128 failureTimer && clearTimeout(failureTimer);
129 callback && callback(...featureValues);
130 resolve(featureValues);
131 }
132 return missingFeatureEntries;
133 }
134 }
135 const missingEntries = tryToResolve();
136 if (missingEntries.length){
137 failureTimer = setTimeout(() => console.error("[FeatureManager] Required features are not available. ",
138 "\nRequested:", featureEntries.map(entry => entry.id),
139 "\nMissing:", missingEntries.filter(entry => entry.state !== 'resolved').map(entry => entry.id)), 1000);
140 missingEntries.forEach(featureEntry => (featureEntry.onAvailable || (featureEntry.onAvailable = U.createCallOnce())).push(tryToResolve));
141 missingEntries.forEach(featureEntry => featureEntry.doLazyResolve && featureEntry.doLazyResolve());
142 }
143 });
144 }
145 /**
146 * Asynchronously create and define a feature that can require other features.
147 * @param {string} featureId
148 * @param {Array<string>} requiredFeatureIds
149 * @param {callable} generatorFn
150 */
151 define(featureId, requiredFeatureIds, generatorFn){
152 const featureEntry = this._getFeatureEntry(featureId);
153 if (featureEntry.state !== undefined && featureEntry.state !== 'requested'){
154 console.error(`[FeatureManager] Feature "${featureId}" already registered:`, featureEntry);
155 }
156 featureEntry.state = 'pending';
157 this.require(requiredFeatureIds, (...featureValues) => this.register(featureId, generatorFn.apply(null, featureValues)));
158 }
159 defineFeature_lazy(featureId, requiredFeatureIds, generatorFn){
160 const featureEntry = this._getFeatureEntry(featureId);
161 if (featureEntry.state === 'requested'){
162 this.define(featureId, requiredFeatureIds, generatorFn);
163 }
164 else {
165 if (featureEntry.state !== undefined){
166 console.error(`[FeatureManager] Feature "${featureId}" already registered:`, featureEntry);
167 }
168 featureEntry.state = 'passive';
169 (featureEntry.doLazyResolve || (featureEntry.doLazyResolve = U.createCallOnce())).push(() =>
170 this.require(requiredFeatureIds)
171 .then(featureValues => this.register(featureId, generatorFn.apply(null, featureValues)))
172 );
173 }
174 }
175 register(featureId, value){
176 const featureEntry = this._getFeatureEntry(featureId);
177 if (featureEntry.state === 'resolved')
178 console.error(`[FeatureManager] Feature "${featureId}" already registered:`, featureEntry);
179 featureEntry.value = value;
180 featureEntry.state = 'resolved';
181 this.dbg && console.log("[FeatureManager]", `New feature "${featureId}"`, value);
182 featureEntry.onAvailable && featureEntry.onAvailable(value);
183 }
184 /**
185 * Get the current value of a string - if the feature is not yet resolved, the value is undefined!
186 * @param {string} featureId
187 */
188 get(featureId){
189 return this._getFeatureEntry(featureId).value;
190 }
191}
192
193{ // features
194 const featureManager = new FeatureManager();
195 webTranslator.defineFeature = featureManager.define.bind(featureManager);
196 webTranslator.getFeature = featureManager.get.bind(featureManager);
197 webTranslator.registerFeature = featureManager.register.bind(featureManager);
198 webTranslator.requireFeatures = featureManager.require.bind(featureManager);
199 webTranslator.withFeatures = featureManager.with.bind(featureManager);
200 webTranslator.features = featureManager;
201}
202
203
204const $rootContainer = $(webTranslator.rootNode);
205
206var isPro = !!dlPageState.pro;
207
208isPro |= dlPageState.loggedIn; // temp until dlPageState.pro is properly set
209
210console.log("DeepL Translator",
211 (isPro?" (DeepL Pro)": ""),
212 (dlPageState.devMode? " (devMode)": ""),
213 (dlPageState.experimental ? "(experimental)" : ""));
214log("Logging enabled.");
215
216
217if (queryVars['reset_tutorial']==="1")
218 localStorage.removeItem("LMT_MessageBox");
219
220
221// = == ========================= Global vars ========================= == =
222
223const CONFIG__SHOW_ALTERNATIVES = 'CONFIG__SHOW_ALTERNATIVES';
224const CONFIG__BLOCK_SCROLLING = 'CONFIG__BLOCK_SCROLLING';
225const CONFIG__HIGHLIGHT = 'CONFIG__HIGHLIGHT';
226const CONFIG__UNDO = 'CONFIG__UNDO';
227const CONFIG__UPDATE_TARGET_SENTENCES = 'CONFIG__UPDATE_TARGET_SENTENCES';
228const CONFIG__ADD_TRANSLATION_FOOTER = 'CONFIG__ADD_TRANSLATION_FOOTER';
229const CONFIG__USE_CONTEXT = 'CONFIG__USE_CONTEXT';
230//const CONFIG__MOBILE_ALTERNATIVES = 'CONFIG__MOBILE_ALTERNATIVES';
231const CONFIG__FAST_TYPING_REQUESTS = 'CONFIG__FAST_TYPING_REQUESTS';
232const CONFIG__FAST_TYPING_REQUESTS__DEV_DUMMY = 'CONFIG__FAST_TYPING_REQUESTS__DEV_DUMMY';
233const CONFIG__CONTEXT_LANG_PAIRS = 'CONFIG__CONTEXT_LANG_PAIRS';
234const CONFIG__SEND_EDIT_LOGS = 'CONFIG__SEND_EDIT_LOGS';
235const CONFIG__IS_APP = 'CONFIG__IS_APP';
236const CONFIG__IS_PRO = 'CONFIG__IS_PRO';
237const CONFIG__IS_DEV = 'CONFIG__IS_DEV';
238const CONFIG__DATA_USE_ALLOWED = 'CONFIG__DATA_USE_ALLOWED';
239const CONFIG__EXPLICITLY_REQUEST_ALTERNATIVES = 'CONFIG__EXPLICITLY_REQUEST_ALTERNATIVES';
240
241const config = webTranslator._config;
242config[CONFIG__SHOW_ALTERNATIVES] = !!queryVars['showAlternatives'];
243config[CONFIG__BLOCK_SCROLLING] = queryVars['blockScrolling']!="0"; // enabled by default
244config[CONFIG__HIGHLIGHT] = queryVars['highlight']!="0"; // enabled by default
245config[CONFIG__UNDO] = queryVars['undo']!="0"; // enabled by default
246config[CONFIG__UPDATE_TARGET_SENTENCES] = queryVars['update']!="0"; // enabled by default
247//config[CONFIG__MOBILE_ALTERNATIVES] = queryVars['mobileAlternatives']!="0" && DeviceProps.isMobilePhone;
248config[CONFIG__ADD_TRANSLATION_FOOTER] = !isPro && !config.disableTranslationByFooter && !DeviceProps.isIPad;
249config[CONFIG__USE_CONTEXT] = queryVars['context'] == '0' ? false : true; // undefined ^= auto
250config[CONFIG__FAST_TYPING_REQUESTS] = true;
251config[CONFIG__FAST_TYPING_REQUESTS__DEV_DUMMY] = !!queryVars['sft']; // (s)imulate (f)ast (t)yping request responses
252config[CONFIG__CONTEXT_LANG_PAIRS] = {
253 // EN_DE : true,
254};
255config[CONFIG__SEND_EDIT_LOGS] = dlPageState.experimental && !isPro && !dlPageState.dataUseProhibited && false;
256config[CONFIG__IS_APP] = config['scrollMode'] === 'app';
257config[CONFIG__IS_PRO] = isPro;
258config[CONFIG__IS_DEV] = dlPageState.devMode;
259config[CONFIG__DATA_USE_ALLOWED] = dlPrivacy.isDataUseAllowed();
260config[CONFIG__EXPLICITLY_REQUEST_ALTERNATIVES] = true;
261config.get = configKey => {
262 if (!config.hasOwnProperty(configKey))
263 throw new Error(`config key '${configKey}' is invalid.`);
264 return config[configKey];
265};
266
267webTranslator.features.register('config', config);
268
269const pubSub = PubSub.create();
270const on = pubSub.listenOn.bind(pubSub);
271const notify = pubSub.notify.bind(pubSub);
272
273const $sourceEdit = $rootContainer.find('.lmt__source_textarea');
274const $targetEdit = $rootContainer.find('.lmt__target_textarea');
275const $translationsAsText = $rootContainer.find('.lmt__translations_as_text');
276
277webTranslator.features.register('domElements', {
278 $rootContainer: $rootContainer,
279 rootContainer: $rootContainer.get(0),
280 $targetEdit: $targetEdit,
281 targetEdit: $targetEdit.get(0),
282 targetEditInnerContainer: $targetEdit.closest(".lmt__inner_textarea_container").get(0),
283 targetEditOuterContainer: $targetEdit.closest(".lmt__textarea_container").get(0),
284 $sourceEdit: $sourceEdit,
285 sourceEdit: $sourceEdit.get(0),
286 sourceEditInnerContainer: $sourceEdit.closest(".lmt__inner_textarea_container").get(0),
287 sourceEditOuterContainer: $sourceEdit.closest(".lmt__textarea_container").get(0),
288 $translationsAsText: $translationsAsText,
289 translationsAsText: $translationsAsText.get(0),
290 sourceSideContainer: $rootContainer.find('.lmt__side_container--source').get(0),
291 targetSideContainer: $rootContainer.find('.lmt__side_container--target').get(0)
292});
293
294var logging = queryVars['logging'];
295
296const TRIGGER__SOURCE_LANG_CHANGED = 'TRIGGER__SOURCE_LANG_CHANGED';
297const TRIGGER__TARGET_LANG_CHANGED = 'TRIGGER__TARGET_LANG_CHANGED';
298const TRIGGER__SOURCE_TEXT_CHANGED_WHILE_TYPING = 'TRIGGER__SOURCE_TEXT_CHANGED_WHILE_TYPING';
299const TRIGGER__SOURCE_TEXT_CHANGED = 'TRIGGER__SOURCE_TEXT_CHANGED';
300const TRIGGER__FAILSAFE_CHECK = 'TRIGGER__FAILSAFE_CHECK';
301
302
303const conf_blockTypingRequestsTime = 10000;
304var conf_sendBackupRequestTime = 1500;
305var conf_typingRequestPriority = -1; // (default: -1) set to -10 to test the rejection of typing requests
306var conf_maxNumCharacters = isPro ? 10000000 : 5000;
307const conf_showLengthMarkerMinLength = isPro ? 1000 : 500;
308var conf_proCharacterAdMinLength = 4000; // TODO: set to 5000
309var conf_translationJobBatchSize = isPro ? 4000: 10000;
310var conf_interceptPastedTextMinLength = 50000;
311var conf_maxSourceLineLength = 14000; // default 14000; choose so that no line request can exceed the rpc limit
312
313
314// = == ========================= end: Global vars ========================= == =
315
316// = == =============================== Global events ================================ == =
317
318/**
319 * @typedef {Object} LMT_OnConststs
320 * @property {string} ON__ANYCLICK - 'ON__ANYCLICK',
321 * @property {string} ON__ACTIVE_ELEMENT_CHANGED - 'ON__ACTIVE_ELEMENT_CHANGED',
322 * @property {string} ON__START_BATCHED_TRANSLATION_REQUEST - 'ON__START_BATCHED_TRANSLATION_REQUEST',
323 * @property {string} ON__CLICK_TARGET_SENTENCE - 'ON__CLICK_TARGET_SENTENCE',
324 * @property {string} ON__INITIAL_SOURCE_TRANSLATION_UPDATED - 'ON__INITIAL_SOURCE_TRANSLATION_UPDATED',
325 * @property {string} ON__INTERCEPT_COPIED_TRANSLATION - 'ON__INTERCEPT_COPIED_TRANSLATION',
326 * @property {string} ON__INTERCEPT_PASTED_TEXT - 'ON__INTERCEPT_PASTED_TEXT',
327 * @property {string} ON__LANGUAGE_CHANGED - 'ON__LANGUAGE_CHANGED',
328 * @property {string} ON__LANGUAGE_SELECT_OPENED - 'ON__LANGUAGE_SELECT_OPENED',
329 * @property {string} ON__NEW_SOURCE_SENTENCE_SPLITTING_RECEIVED - 'ON__NEW_SOURCE_SENTENCE_SPLITTING_RECEIVED',
330 * @property {string} ON__PARTIAL_TRANSLATION_RECEIVED - 'ON__PARTIAL_TRANSLATION_RECEIVED',
331 * @property {string} ON__REQUEST_STARTED - 'ON__REQUEST_STARTED',
332 * @property {string} ON__REQUEST_ENDED - 'ON__REQUEST_ENDED',
333 * @property {string} ON__REQUEST_FAILED - 'ON__REQUEST_FAILED',
334 * @property {string} ON__RPC_CALL_SUCCESSFULL - 'ON__RPC_CALL_SUCCESSFULL',
335 * @property {string} ON__RPC_CALL_FAILED - 'ON__RPC_CALL_FAILED',
336 * @property {string} ON__SOURCE_TEXT_CHANGED - 'ON__SOURCE_TEXT_CHANGED',
337 * @property {string} ON__SOURCE_SENTENCES_MAY_REQUIRE_UPDATE
338 * Sentence splitting or translations may have become invalid.
339 * Can e.g. be fired after ON__SOURCE_TEXT_CHANGED
340 * @property {string} ON__SOURCE_TEXT_EMPTY - 'ON__SOURCE_TEXT_EMPTY',
341 * @property {string} ON__SOURCE_SENTENCES_CHANGED - 'ON__SOURCE_SENTENCES_CHANGED',
342 * @property {string} ON__TARGET_POPUP_OPENED - 'ON__TARGET_POPUP_OPENED',
343 * @property {string} ON__TARGET_POPUP_CLOSED - 'ON__TARGET_POPUP_CLOSED',
344 * @property {string} ON__TARGET_EDIT_TEXT_CHANGED - 'ON__TARGET_EDIT_TEXT_CHANGED',
345 * @property {string} ON__TARGET_EDIT_KEY_UP - 'ON__TARGET_EDIT_KEY_UP',
346 * @property {string} ON__TARGET_EDIT_KEY_DOWN - 'ON__TARGET_EDIT_KEY_DOWN',
347 * @property {string} ON__TEXT_AREAS_RESTYLED - Called after size or font changed (called by recalculateFontSizeAndScrollHeight)
348 * @property {string} ON__USER_EVENT - Called when an event is triggered that should be
349 * handled outside of the translator's context
350 * e.g. if the user requests to open the contract page in the app.
351 */
352
353/**
354 * (devMode) Freeze the object when in dev mode to detect programming errors.
355 * - throws an exception when trying to access a non existing property
356 * - throws an exception when trying to set ANY property on the object
357 * @param {Object} obj
358 * @return {Proxy<Object>| Object} a Wrapper
359 */
360function dev_freezeProxy(obj){
361 return (dlPageState.devMode && window.Proxy) ? new Proxy(obj, {
362 get: function(target, propertyName){
363 if(propertyName in target){
364 return target[propertyName];
365 }else {
366 throw new Error("[dev_freezeProxy] Property error '" + propertyName + "'");
367 }
368 },
369 set: function(_target, propertyName, _value){
370 throw new Error("[dev_freezeProxy] Can't set property '" + propertyName + "' on frozen object.");
371 }
372 }) : obj;
373}
374
375/**
376 * (devMode) Seal the object when in dev mode to detect programming errors.
377 * - throws an exception when trying to access a non existing property
378 * - throws an exception when trying to set a NEW property on the object
379 * @param {Object} obj
380 * @return {Proxy<Object>| Object} a Wrapper
381 */
382function dev_sealProxy(obj){
383 return (dlPageState.devMode && window.Proxy)? new Proxy(obj, {
384 get: function(target, propertyName){
385 if(propertyName in target){
386 return target[propertyName];
387 }else {
388 throw new Error("[dev_sealProxy] Property error '" + propertyName + "'");
389 }
390 },
391 set: function(target, propertyName, value){
392 if(target.hasOwnProperty(propertyName)){
393 target[propertyName] = value;
394 return true;
395 }
396 throw new Error("[dev_sealProxy] Can't set property '" + propertyName + "' on sealed object.");
397 }
398 }) : obj;
399}
400
401/** @type {LMT_OnConststs} */
402const ON = dev_freezeProxy({
403 ON__ANYCLICK: 'ON__ANYCLICK',
404 ON__ACTIVE_ELEMENT_CHANGED: 'ON__ACTIVE_ELEMENT_CHANGED',
405 ON__START_BATCHED_TRANSLATION_REQUEST: 'ON__START_BATCHED_TRANSLATION_REQUEST',
406 ON__CLICK_TARGET_SENTENCE: 'ON__CLICK_TARGET_SENTENCE',
407 ON__INITIAL_SOURCE_TRANSLATION_UPDATED: 'ON__INITIAL_SOURCE_TRANSLATION_UPDATED',
408 ON__INTERCEPT_COPIED_TRANSLATION: 'ON__INTERCEPT_COPIED_TRANSLATION',
409 ON__INTERCEPT_PASTED_TEXT: 'ON__INTERCEPT_PASTED_TEXT',
410 ON__LANGUAGE_CHANGED: 'ON__LANGUAGE_CHANGED',
411 ON__LANGUAGE_SELECT_OPENED: 'ON__LANGUAGE_SELECT_OPENED',
412 ON__NEW_SOURCE_SENTENCE_SPLITTING_RECEIVED: 'ON__NEW_SOURCE_SENTENCE_SPLITTING_RECEIVED',
413 ON__NO_ALTERNATIVES_FOUND: 'ON__NO_ALTERNATIVES_FOUND',
414 ON__PARTIAL_TRANSLATION_RECEIVED: 'ON__PARTIAL_TRANSLATION_RECEIVED',
415 ON__REQUEST_STARTED: 'ON__REQUEST_STARTED',
416 ON__REQUEST_ENDED: 'ON__REQUEST_ENDED',
417 ON__REQUEST_FAILED: 'ON__REQUEST_FAILED',
418 ON__RPC_CALL_SUCCESSFULL: 'ON__RPC_CALL_SUCCESSFULL',
419 ON__RPC_CALL_FAILED: 'ON__RPC_CALL_FAILED',
420 ON__SOURCE_TEXT_CHANGED: 'ON__SOURCE_TEXT_CHANGED',
421 ON__SOURCE_TEXT_EMPTY: 'ON__SOURCE_TEXT_EMPTY',
422 ON__SOURCE_SENTENCES_CHANGED: 'ON__SOURCE_SENTENCES_CHANGED',
423 ON__SOURCE_SENTENCES_MAY_REQUIRE_UPDATE: 'ON__SOURCE_SENTENCES_MAY_REQUIRE_UPDATE',
424 ON__TARGET_POPUP_OPENED: 'ON__TARGET_POPUP_OPENED',
425 ON__TARGET_POPUP_CLOSED: 'ON__TARGET_POPUP_CLOSED',
426 ON__TARGET_EDIT_TEXT_CHANGED: 'ON__TARGET_EDIT_TEXT_CHANGED',
427 ON__TARGET_EDIT_KEY_UP: 'ON__TARGET_EDIT_KEY_UP',
428 ON__TARGET_EDIT_KEY_DOWN: 'ON__TARGET_EDIT_KEY_DOWN',
429 ON__TRANSLATION_READY: 'ON__TRANSLATION_READY',
430 ON__TEXT_AREAS_RESTYLED: 'ON__TEXT_AREAS_RESTYLED',
431 ON__USER_EDIT_PERFORMED: 'ON__USER_EDIT_PERFORMED',
432 ON__USER_EVENT: 'ON__USER_EVENT',
433});
434
435webTranslator.features.register('eventHandling', {
436 on,
437 notify,
438 ON
439});
440
441// = == =============================== end: Global events ================================ == =
442
443// = == =============================== Data ================================ == =
444let _uid = 0;
445
446function counter(){ return ++counter._counter; }
447counter._counter = 0;
448
449const GSTATE__FORCE_SENTENCE_SPLITTING_REQUESTS_FOR_STABILITY = 'GSTATE__FORCE_SENTENCE_SPLITTING_REQUESTS_FOR_STABILITY';
450const GSTATE__TYPING_REQUESTS_BLOCKED_UNTIL = 'GSTATE__TYPING_REQUESTS_BLOCKED_UNTIL';
451const GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE = 'GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE';
452const GSTATE__EDIT_FEATURES_DISABLED = 'GSTATE__EDIT_FEATURES_DISABLED';
453const GSTATE__ADDITIONAL_JOB_REQUEST_PARAMETERS = 'GSTATE__ADDITIONAL_JOB_REQUEST_PARAMETERS';
454
455function GlobalState(){
456 this[GSTATE__FORCE_SENTENCE_SPLITTING_REQUESTS_FOR_STABILITY] = U.value(false);
457 this[GSTATE__TYPING_REQUESTS_BLOCKED_UNTIL] = 0;
458 this[GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE] = U.value(false);
459 this[GSTATE__EDIT_FEATURES_DISABLED] = U.value(false);
460 this[GSTATE__ADDITIONAL_JOB_REQUEST_PARAMETERS] = {};
461}
462const globalState = new GlobalState;
463webTranslator.globalState = globalState;
464
465
466// ==========================
467
468
469/**
470 * @typedef {Object} LMT_State
471 * @property {mixed} STATE__TRANSLATION_REGISTRY - trimmed source sentence -> LMT_Translation object
472 * @property {array} STATE__SOURCE_SENTENCES - ?
473 * @property {array} STATE__TARGET_SENTENCES - ?
474 * @property {string} STATE__TARGET_TEXT - ?
475 * @property {mixed} STATE__MODEL_CONFIG - ?
476 */
477
478const STATE__TRANSLATION_REGISTRY = 'STATE__TRANSLATION_REGISTRY';
479const STATE__SOURCE_SENTENCES = 'STATE__SOURCE_SENTENCES';
480const STATE__TARGET_SENTENCES = 'STATE__TARGET_SENTENCES';
481const STATE__TARGET_TEXT = 'STATE__TARGET_TEXT';
482const STATE__MODEL_CONFIG = 'STATE__MODEL_CONFIG';
483
484class State {
485 /**
486 * @lends LMT_State
487 * @param {string} sourceLang
488 * @param {string} targetLang
489 * @constructor
490 */
491 constructor(sourceLang, targetLang){
492 this._id = "State_#" + (_uid++) + "_"+sourceLang + "_"+targetLang; // mainly for debugging...
493 this._sourceLang = sourceLang;
494 this._targetLang = targetLang;
495 this[STATE__TRANSLATION_REGISTRY] = new Object(null); // trimmed source sentence -> Translation object
496 this[STATE__SOURCE_SENTENCES] = [];
497 this[STATE__TARGET_SENTENCES] = [];
498 this[STATE__TARGET_TEXT] = undefined;
499 // this._contextEnabled = config[CONFIG__USE_CONTEXT] || (config[CONFIG__CONTEXT_LANG_PAIRS][sourceLang + "_" + targetLang] && config[CONFIG__USE_CONTEXT]!==false);
500 this._contextEnabled = config[CONFIG__USE_CONTEXT];
501 this[STATE__MODEL_CONFIG] = new Object(null);
502 log("Created new State ", this._id, this.isContextEnabled()?"context":"nocontext");
503 }
504 getId(){
505 return this._id;
506 }
507 getLangConfigForTranslation(){
508 return {
509 'source_lang_computed': this._sourceLang,
510 'target_lang': this._targetLang,
511 };
512 }
513 getSourceLang(){
514 return this._sourceLang;
515 }
516 getTargetLang(){
517 return this._targetLang;
518 }
519 getModelConfig(){
520 return this[STATE__MODEL_CONFIG];
521 }
522 getTargetSentences(){
523 return this[STATE__TARGET_SENTENCES];
524 }
525 getSourceSentences(){
526 return this[STATE__SOURCE_SENTENCES];
527 }
528 createJobSettings(langConfig=null, priority=1.0) {
529 return {
530 langConfig: langConfig !== null ? langConfig : this.getLangConfigForTranslation(),
531 modelConfig: this.getModelConfig(),
532 priority: priority,
533 requestParameters: globalState[GSTATE__ADDITIONAL_JOB_REQUEST_PARAMETERS],
534 };
535 }
536 isContextEnabled(){
537 return this._contextEnabled;
538 }
539 clearSentences(){
540 this[STATE__SOURCE_SENTENCES].splice(0);
541 this[STATE__TARGET_SENTENCES].splice(0);
542 this[STATE__TARGET_TEXT] = "";
543 }
544}
545
546
547
548// = == =============== stateManagement =============== == =
549
550
551webTranslator.features.define('stateManagement',
552 ['webTranslatorCore', 'eventHandling', 'config'],
553 (webTranslatorCore, {ON, notify}, config) => {
554
555 const dbg = false && config.get('CONFIG__IS_DEV')
556 const TAG = "[stateManagement]"
557 const activeState = U.value(_createState("", "")) // init with empty lang pair
558
559 webTranslatorCore.activeState = activeState
560
561 U.withValue(activeState, newState => {
562 window._tState = newState // for debugging
563 // dbg && console.log(TAG, "Set new State ", sourceLang, targetLang)
564 webTranslatorCore.sourceLang(newState.getSourceLang())
565 webTranslatorCore.targetLang(newState.getTargetLang())
566 if (newState.getSourceLang() && newState.getTargetLang()){
567 notify(ON.ON__LANGUAGE_CHANGED, newState.getSourceLang(), newState.getTargetLang())
568 setTimeout(() => U.notify('LMT/langChanged'), 100) // deprecated
569 }
570 })
571
572 // trigger state reset (and re-translation) if another source language is selected
573 let _prevSelectedSourceLang = webTranslatorCore.getSelectedSourceLang()
574
575 // onSelectedSourceLangHasBeenChangedExplicitly
576 webTranslatorCore.onSourceLangSelected.push(newSelectedSourceLang => {
577 dbg && console.log(TAG, "SelectedSourceLang changed to ", newSelectedSourceLang)
578 const state = activeState()
579
580 const prevSourceLang = _prevSelectedSourceLang === "auto" ? (state.getSourceLang() || "") : _prevSelectedSourceLang
581 _prevSelectedSourceLang = newSelectedSourceLang
582 if (newSelectedSourceLang === 'auto'){ // source lang auto detection is required
583 updateActiveLanguages("", "") // invalidate lang pair to force re-detection
584 }
585 else if (newSelectedSourceLang === webTranslatorCore.getSelectedTargetLang()) { // handle sourceLang == targetLang case
586 dbg && console.log("Switching target language, too. lastSourceLang="+prevSourceLang+", webTranslator.selectedSourceLang="+webTranslatorCore.getSelectedSourceLang())
587 webTranslatorCore.setSelectedTargetLang( (prevSourceLang === 'auto' || prevSourceLang === webTranslatorCore.getSelectedSourceLang()) ? "EN" : prevSourceLang)
588 // -> this should trigger setting a new state
589 }
590 else if (newSelectedSourceLang === state.getSourceLang()){ // actual source lang did not change... do nothing
591 dbg && console.log(TAG, "Keep source language.")
592 }
593 else {
594 //updateActiveLanguages(newSelectedSourceLang, activeState().getTargetLang() || webTranslatorCore.getSelectedSourceLang())
595 updateActiveLanguages(newSelectedSourceLang, webTranslatorCore.getSelectedTargetLang())
596 }
597 })
598
599 webTranslatorCore.selectedTargetLang.onValueChanged.push((newSelectedTargetLang) => {
600 const currentSourceLang = activeState().getSourceLang()
601 const currentTargetLang = activeState().getTargetLang()
602
603 if (newSelectedTargetLang !== currentTargetLang){
604 dbg && console.log(TAG, "SelectedTargetLang changed to ", newSelectedTargetLang)
605 updateActiveLanguages(currentSourceLang, newSelectedTargetLang)
606 // notify(ON.ON__SOURCE_TEXT_CHANGED, new RequestContext(activeState(), TRIGGER__TARGET_LANG_CHANGED))
607 setTimeout(() => U.notify('LMT/langChanged'), 100) // deprecated
608 }
609 })
610
611 webTranslatorCore.isActiveState = state => state === activeState()
612 webTranslatorCore.getActiveState = () => activeState()
613
614
615 /**
616 * @param {string} sourceLang
617 * @param {string} targetLang
618 * @return {LMT_State}
619 */
620 function _createState(sourceLang, targetLang){
621 return dev_sealProxy(new State(sourceLang, targetLang))
622 }
623
624
625 /**
626 * @param {string} sourceLang || ""
627 * @param {string} targetLang || ""
628 * @note triggers ON.ON__SOURCE_TEXT_CHANGED
629 */
630 function updateActiveLanguages(newSourceLang, newTargetLang){
631 dbg && console.log(TAG, 'updateActiveLanguages', newSourceLang, newTargetLang)
632
633 const prevState = activeState()
634 const prevSourceLang = prevState.getSourceLang()
635 const prevTargetLang = prevState.getTargetLang()
636
637 if (newSourceLang !== prevSourceLang || newTargetLang !== prevTargetLang){
638 activeState(_createState(newSourceLang, newTargetLang))
639
640 if (newSourceLang && newSourceLang !== webTranslatorCore.getSelectedSourceLang() && webTranslatorCore.getSelectedSourceLang() !== 'auto'){
641 webTranslatorCore._selectedSourceLang('auto') // , explicit = false
642 }
643 if (newTargetLang){
644 webTranslatorCore.setSelectedTargetLang(newTargetLang)
645 }
646 notify(ON.ON__SOURCE_TEXT_CHANGED, new RequestContext(activeState(),
647 newSourceLang !== prevSourceLang
648 ? TRIGGER__SOURCE_LANG_CHANGED
649 : TRIGGER__TARGET_LANG_CHANGED))
650 }
651 }
652
653 function changedSourceLangDetected(newSourceLang){
654 dbg && console.log(TAG, 'changedSourceLangDetected', newSourceLang)
655
656 const state = activeState()
657 const currentSourceLang = state.getSourceLang()
658 const currentSelectedTargetLang = webTranslatorCore.getSelectedTargetLang()
659 const currentSelectedSourceLang = webTranslatorCore.getSelectedSourceLang()
660
661 if (newSourceLang === currentSourceLang){
662 // ignore
663 }
664 else if (newSourceLang === currentSelectedTargetLang){
665 // new target:
666 const newTargetLang = currentSourceLang || // switch if possible
667 webTranslatorCore // highest prio preferred != newSourceLang
668 .getPreferredLangs()
669 .filter(lang => lang!=newSourceLang)
670 .pop()
671 webTranslatorCore._selectedSourceLang("auto")
672 updateActiveLanguages(newSourceLang, newTargetLang)
673 }
674 else {
675 if (newSourceLang !== currentSelectedSourceLang)
676 webTranslatorCore._selectedSourceLang('auto')
677 updateActiveLanguages(newSourceLang, currentSelectedTargetLang)
678 }
679 }
680
681
682 function onUserSelectsSourceLang(newSelectedSourceLang){
683 dbg && console.log('onUserSelectsSourceLang', newSelectedSourceLang)
684
685 const currentSelectedSourceLang = webTranslatorCore.getSelectedSourceLang()
686 const currentSelectedTargetLang = webTranslatorCore.getSelectedTargetLang()
687
688 if (newSelectedSourceLang === currentSelectedSourceLang){
689 // ignore
690 }
691 else if (newSelectedSourceLang === currentSelectedTargetLang){
692 const newSelectedTargetLang = currentSelectedSourceLang !== 'auto'
693 ? currentSelectedSourceLang
694 : webTranslatorCore // highest prio preferred != newSourceLang
695 .getPreferredLangs()
696 .filter(lang => lang != newSelectedSourceLang)
697 .pop()
698
699 webTranslatorCore.pushPreferredLang(newSelectedSourceLang)
700 webTranslatorCore._selectedSourceLang(newSelectedSourceLang)
701 updateActiveLanguages("", newSelectedTargetLang)
702
703 }
704 else {
705 webTranslatorCore.pushPreferredLang(newSelectedSourceLang)
706 webTranslatorCore.selectSourceLang(newSelectedSourceLang)
707 }
708 }
709
710 function onUserSelectsTargetLang(newSelectedTargetLang){
711 dbg && console.log(TAG, 'onUserSelectsTargetLang', newSelectedTargetLang)
712
713 const state = activeState()
714 const currentSelectedTargetLang = webTranslatorCore.getSelectedTargetLang()
715 const currentSourceLang = state.getSourceLang()
716
717 if (newSelectedTargetLang === currentSelectedTargetLang){
718 // ignore
719 }
720 // else
721 else {
722 webTranslatorCore.pushPreferredLang(newSelectedTargetLang)
723 if (currentSourceLang !== newSelectedTargetLang){
724 updateActiveLanguages(currentSourceLang, newSelectedTargetLang)
725 }
726 else {
727 webTranslatorCore._selectedSourceLang("auto")
728 updateActiveLanguages("", newSelectedTargetLang)
729 }
730 // webTranslatorCore.setSelectedTargetLang(newSelectedTargetLang)
731 }
732 }
733
734 function getPrioritizedSourceLangs(){
735 const state = activeState()
736 const userPreferredLangs = webTranslatorCore.getPreferredLangs()
737
738 let highesPriorityGuessForLangDetection
739 if (state.getSourceLang()){ // prefer active language
740 highesPriorityGuessForLangDetection = state.getSourceLang()
741 }
742 else if (webTranslatorCore.getSelectedSourceLang() !== 'auto') {
743 highesPriorityGuessForLangDetection = webTranslatorCore.getSelectedSourceLang()
744 }
745 else { // try to prevent lang switch by using highest priority lang that is not the target lang
746 const selectedTargetLang = webTranslatorCore.getSelectedTargetLang()
747 highesPriorityGuessForLangDetection = userPreferredLangs
748 .filter(lang => lang !== selectedTargetLang)
749 .pop()
750 }
751 const prioritizedSourceLangs = userPreferredLangs.filter(lang => lang !== highesPriorityGuessForLangDetection)
752 prioritizedSourceLangs.push(highesPriorityGuessForLangDetection)
753 return prioritizedSourceLangs
754 }
755
756 function getBestPreferredLang(matcher=null){
757 const langs = webTranslatorCore.getPreferredLangs()
758 if (!matcher)
759 return langs[langs.length-1] ||''
760 for (let i = langs.length - 1; i >= 0; --i)
761 if (matcher(langs[i]))
762 return langs[i]
763 return ''
764 }
765
766 /** return a supported upper cased lang code or '' */
767 function sanitizeLangCode(langCode){
768 langCode = ('' + (langCode || '')).substr(0, 2).toUpperCase()
769 return webTranslatorCore._supportedLangCodes[langCode] ? langCode: ''
770 }
771
772 return {
773 updateActiveLanguages,
774 changedSourceLangDetected,
775 onUserSelectsSourceLang,
776 onUserSelectsTargetLang,
777 getPrioritizedSourceLangs,
778 sanitizeAndUpdateActiveLanguages(givenLang1, givenLang2) {
779 dbg && console.log(TAG, "sanitizeAndUpdateActiveLanguages: enter", givenLang1, givenLang2)
780
781 const lang1 = sanitizeLangCode(givenLang1)
782 let lang2 = sanitizeLangCode(givenLang2)
783
784 if (!lang2 || lang1 === lang2) {
785 lang2 = getBestPreferredLang(lc => lc !== lang1)
786 }
787
788 dbg && console.log(TAG, "sanitizeAndUpdateActiveLanguages: done", lang1, lang2)
789
790 updateActiveLanguages(lang1, lang2)
791 }
792 }
793})
794
795// = == =============== end: stateManagement =============== == =
796
797
798
799
800
801
802// ==========================
803// Data
804
805/**
806 Rules for whitespaces and sentences:
807 - All ws at the beginning of a text belong to the first sentence
808 - All ws at the end of a text belong to the last sentence
809 - If two sentence are separated by spaces, the first space belongs to the left sentence, all other spaces to the right sentence
810 - If there is a linebreak between two sentences, all spaces up to and including the first line break belong to the left sentence,
811 the remaining ws belong to the right sentence
812
813 \note Make sure that these rules apply to source- and target sentences in exactly the same way!
814*/
815
816const REGEX_sentenceBeginningWhitespaces = /^\s*/g; // all whitespaces before a sentence (matches always)
817// // all whitespaces after a sentence; this includes all empty lines after a sentence but excludes the whitespaces in the same line before the next sentence
818// const REGEX_sentenceEndingWhitespaces = /^\s*(\n(?=[ \t]*\S)|$)/g;
819
820
821// all whitespaces after a sentence; this includes all whitespaces to the end of the line (including the newline), the first of multiple spaces if there is more than one or an empty string
822const REGEX_sentenceEndingWhitespaces = /^((\s*$)|(\s*?\n)|( ?)(?= +))/g;
823
824
825const REGEX_sentenceEndingWhitespacesBack = /\s*$/; // all whitespaces at the end of a string (matches always)
826const REGEX_sentenceBeginningWhitespacesBack = /(?:^|\S)(?:\s*?\n| ?(?= ))(\s*)$/; // ending whitespaces excluding REGEX_sentenceBeginningWhitespaces; result in m[1]
827
828const REGEX_collapseWhitspaces = /\s+/g;
829
830
831const REGEX_textPartMatch = /\s+(?=\S|$)|\S+(?=\s|$)/g; // all string and non-string blocks (matches always)
832/**
833 * Split a text into whitespace and non-whitespace parts
834 * @param {string} text
835 * @returns {Array<Part>} [{isWhitespacePart: bool, text: string}]
836 */
837function splitTextIntoParts(text){
838 var parts = [];
839 text.replace(REGEX_textPartMatch, function(s, _offset){
840 parts.push({
841 isWhitespacePart: !!s.match(/\s/),
842 text: s
843 });
844 });
845 return parts;
846}
847
848// ------------
849
850/**
851 * @typedef {Object} LMT_Translation
852 * @property {id} TRANSLATION__ID - session-unique id
853 * @property {string} TRANSLATION__STATUS - the current status TRANSLATION_STATUS__xxx of the translation
854 * @property {string} TRANSLATION__SOURCE_TEXT - trimmed source text
855 * @property {string} TRANSLATION__INITIAL_TRANSLATION_TEXT - ?
856 * @property {Array<string>} TRANSLATION__ALTERNATIVE_INITIAL_TRANSLATION_TEXTS - ?
857 * @property {Object} TRANSLATION__PREFIXES_WITH_REQUESTED_LOCAL_ALTERNATIVES - ?
858 * @property {Object} TRANSLATION__INITIAL_TRANSLATION_RESULT - ?
859 */
860const TRANSLATION__ID = 'TRANSLATION__ID';
861const TRANSLATION__STATUS = 'TRANSLATION__STATUS';
862const TRANSLATION__SOURCE_TEXT = 'TRANSLATION__SOURCE_TEXT';
863const TRANSLATION__INITIAL_TRANSLATION_TEXT = 'TRANSLATION__INITIAL_TRANSLATION_TEXT';
864const TRANSLATION__ALTERNATIVE_INITIAL_TRANSLATION_TEXTS = 'TRANSLATION__ALTERNATIVE_INITIAL_TRANSLATION_TEXTS';
865const TRANSLATION__PREFIXES_WITH_REQUESTED_LOCAL_ALTERNATIVES = 'TRANSLATION__PREFIXES_WITH_REQUESTED_LOCAL_ALTERNATIVES';
866const TRANSLATION__INITIAL_TRANSLATION_RESULT = 'TRANSLATION__INITIAL_TRANSLATION_RESULT';
867
868const TRANSLATION_STATUS__NEW = 'TRANSLATION_STATUS__NEW';
869const TRANSLATION_STATUS__READY = 'TRANSLATION_STATUS__READY';
870const TRANSLATION_STATUS__READY__FAST_QUALITY = 'TRANSLATION_STATUS__READY__FAST_QUALITY';
871const TRANSLATION_STATUS__REQUESTES_FAST__DEV_TEST = 'TRANSLATION_STATUS__REQUESTES_FAST__DEV_TEST';
872// const TRANSLATION__TARGETS_WITH_REQUESTED_FULL_ALTERNATIVES = 'altRq';
873
874function Translation(sourceText){
875 this[TRANSLATION__ID] = _uid++;
876 this[TRANSLATION__STATUS] = TRANSLATION_STATUS__NEW;
877 this[TRANSLATION__SOURCE_TEXT] = sourceText;
878 this[TRANSLATION__PREFIXES_WITH_REQUESTED_LOCAL_ALTERNATIVES] = new Object(null);
879 this[TRANSLATION__INITIAL_TRANSLATION_RESULT] = undefined;
880 this[TRANSLATION__INITIAL_TRANSLATION_TEXT] = undefined;
881 this[TRANSLATION__ALTERNATIVE_INITIAL_TRANSLATION_TEXTS] = undefined;
882 // this[TRANSLATION__TARGETS_WITH_REQUESTED_FULL_ALTERNATIVES] = new Object(null);
883}
884/** @return {LMT_Translation} */
885function createTranslation(sourceText){
886 return dev_sealProxy(new Translation(sourceText));
887}
888
889/**
890 * @typedef {Object} LMT_SourceSentence
891 * @property {id} S_SENTENCE__ID - session-unique id
892 * @property {LMT_Translation} S_SENTENCE__TRANSLATION -?
893 * @property {Array<LMT_Translation> | undefined} S_SENTENCE__PREVIOUS_TRANSLATIONS - ?
894 * @property {string} S_SENTENCE__WHITESPACES_BEFORE
895 * @property {string} S_SENTENCE__WHITESPACES_AFTER
896 * @property {int} S_SENTENCE__WHITESPACES_BEFORE_REV
897 * @property {int} S_SENTENCE__WHITESPACES_AFTER_REV
898 * @property {string} S_SENTENCE__RAW_TRIMMED_TEXT
899 * @property {int} S_SENTENCE__EDIT_REV
900 */
901const S_SENTENCE__ID = 'id';
902const S_SENTENCE__TRANSLATION = 'translations';
903const S_SENTENCE__PREVIOUS_TRANSLATIONS = 'preTranslations';
904const S_SENTENCE__WHITESPACES_BEFORE = 'wsBefore';
905const S_SENTENCE__WHITESPACES_AFTER = 'wsAfter';
906
907const S_SENTENCE__RAW_TRIMMED_TEXT = 'rawText';
908const S_SENTENCE__EDIT_REV = 'sRev';
909const S_SENTENCE__WHITESPACES_BEFORE_REV = 'wsBeforeRev';
910const S_SENTENCE__WHITESPACES_AFTER_REV = 'wsAfterRev';
911
912const S_SENTENCE__PREV_SENTENCE = 'S_SENTENCE__PREV_SENTENCE';
913const S_SENTENCE__NEXT_SENTENCE = 'S_SENTENCE__NEXT_SENTENCE';
914
915// var S_SENTENCE__QUICK_TYPING_TRANSLATION = 'typingTranslations'; ??? could be used for typing requests
916
917// ------------
918
919/**
920 * @lends LMT_SourceSentence
921 * @param {LMT_Translation} translation
922 * @class
923 */
924function SourceSentence(){
925 this[S_SENTENCE__ID] = _uid++;
926 this[S_SENTENCE__TRANSLATION] = undefined;
927 this[S_SENTENCE__PREVIOUS_TRANSLATIONS] = undefined;
928 this[S_SENTENCE__WHITESPACES_BEFORE] = "";
929 this[S_SENTENCE__WHITESPACES_AFTER] = "";
930 this[S_SENTENCE__RAW_TRIMMED_TEXT] = "";
931 this[S_SENTENCE__EDIT_REV] = counter();
932 this[S_SENTENCE__WHITESPACES_BEFORE_REV] = this[S_SENTENCE__EDIT_REV];
933 this[S_SENTENCE__WHITESPACES_AFTER_REV] = this[S_SENTENCE__EDIT_REV];
934 this[S_SENTENCE__PREV_SENTENCE] = undefined;
935 this[S_SENTENCE__NEXT_SENTENCE] = undefined;
936
937}
938SourceSentence.prototype.updateWhitespacesAfter = function _setWsAfter(ws){
939 if (ws!==this[S_SENTENCE__WHITESPACES_AFTER]){
940 this[S_SENTENCE__WHITESPACES_AFTER] = ws;
941 this[S_SENTENCE__WHITESPACES_AFTER_REV] = counter();
942 }
943};
944SourceSentence.prototype.updateWhitespacesBefore = function _setWsBefore(ws){
945 if (ws!==this[S_SENTENCE__WHITESPACES_BEFORE]){
946 this[S_SENTENCE__WHITESPACES_BEFORE] = ws;
947 this[S_SENTENCE__WHITESPACES_BEFORE_REV] = counter();
948 }
949};
950SourceSentence.prototype.getText = function _getText(){
951 return this[S_SENTENCE__WHITESPACES_BEFORE] + this[S_SENTENCE__RAW_TRIMMED_TEXT] + this[S_SENTENCE__WHITESPACES_AFTER];
952};
953SourceSentence.prototype.getNormalizedText = function _getNormalizedText(){
954 return this[S_SENTENCE__RAW_TRIMMED_TEXT].replace(REGEX_collapseWhitspaces, " ");
955}
956/**
957 * @param {LMT_Translation} translation
958 * @return {LMT_SourceSentence} a new SourceSentence
959 */
960function createSourceSentence(){
961 return dev_sealProxy(new SourceSentence);
962}
963/**
964 * @param {LMT_SourceSentence} sourceSentence
965 * @return {Object} {prefix: Array<string>, postfix: Array<string>}
966 */
967function doGetSourceContext(sourceSentence){
968 var prefix = [],
969 prefixTextLength = 0,
970 s = sourceSentence[S_SENTENCE__PREV_SENTENCE];
971 while (s && prefix.length<5){ // <= 5 sentences before
972 var text = s.getNormalizedText();
973 prefixTextLength += text.length;
974 if (prefixTextLength>1000) // <= 1000 characters
975 break;
976 prefix.unshift(text);
977 s = s[S_SENTENCE__PREV_SENTENCE];
978 }
979
980 var postfix = [];
981 sourceSentence[S_SENTENCE__NEXT_SENTENCE] &&
982 postfix.push(sourceSentence[S_SENTENCE__NEXT_SENTENCE].getNormalizedText().substr(0, 500)); // <= 500 characters
983 return {
984 prefix: prefix,
985 postfix: postfix
986 };
987}
988
989// ------------
990
991/**
992 * @typedef {Object} LMT_TargetSentence
993 * @property {id} TRANSLATION__ID - session-unique id
994 * @property {string} T_SENTENCE__TEXT - raw target text (not normalized with whitespaces before and after)
995 * @property {Array<LMT_SourceSentence>} T_SENTENCE__ALIGNED_SOURCE_SENTENCES
996 * @property {Array<String>} T_SENTENCE__EDIT_SNAPSHOTS - ?
997 * @property {string} T_SENTENCE__LAST_FULL_TEXT - last full text as received by the server
998 * @property {int} T_SENTENCE__EDIT_REV - ?
999 * @property {Array} T_SENTENCE__AUTOCOMPLETION_RESULTS - [ [requestedPrefix, fullSentence, result] ]
1000 * @property {int} T_SENTENCE__WHITESPACES_BEFORE_REV - ?
1001 * @property {int} T_SENTENCE__WHITESPACES_AFTER_REV - ?
1002 */
1003
1004const T_SENTENCE__ID = 'id';
1005const T_SENTENCE__TEXT = 'tText';
1006const T_SENTENCE__ALIGNED_SOURCE_SENTENCES = 'sSentences'; // [ [score, sentence]* ]
1007const T_SENTENCE__PARTS = 'parts';
1008const T_SENTENCE__EDIT_SNAPSHOTS = 'editSnapshots';
1009const T_SENTENCE__LAST_FULL_TEXT = 'lastFullText'; // last full normalized version received by the server used to detect where the sentence ends
1010const T_SENTENCE__AUTOCOMPLETION_RESULTS = 'acResults';
1011
1012const T_SENTENCE__EDIT_REV = 'tsRev';
1013const T_SENTENCE__WHITESPACES_BEFORE_REV = 'tsWsBRev';
1014const T_SENTENCE__WHITESPACES_AFTER_REV = 'tsWsARev';
1015
1016/**
1017 * @lends LMT_TargetSentence
1018 * @param {string} text
1019 * @constructor
1020 */
1021function TargetSentence(text){
1022 this[T_SENTENCE__ID] = _uid++;
1023 this[T_SENTENCE__TEXT] = text;
1024 this[T_SENTENCE__PARTS] = undefined;
1025 this[T_SENTENCE__ALIGNED_SOURCE_SENTENCES] = []; // [ sourceSentence* ] (first one is the active one)
1026 this[T_SENTENCE__EDIT_SNAPSHOTS] = [];
1027 this[T_SENTENCE__LAST_FULL_TEXT] = text.trim();
1028 this[T_SENTENCE__EDIT_REV] = -1;
1029 this[T_SENTENCE__AUTOCOMPLETION_RESULTS] = []; // [ [requestedPrefix, fullSentence, result] ]
1030 this[T_SENTENCE__WHITESPACES_BEFORE_REV] = -1;
1031 this[T_SENTENCE__WHITESPACES_AFTER_REV] = -1;
1032}
1033TargetSentence.prototype.getSourceSentence = function _getSourceSentence(){
1034 return this[T_SENTENCE__ALIGNED_SOURCE_SENTENCES][0];
1035};
1036
1037TargetSentence.prototype.getTranslation = function _getTranslation(){
1038 var bestSourceSentence = this[T_SENTENCE__ALIGNED_SOURCE_SENTENCES][0];
1039 return bestSourceSentence && bestSourceSentence[S_SENTENCE__TRANSLATION];
1040};
1041
1042TargetSentence.prototype.setSourceSentence = function _setTranslation(sSentence){
1043 if (this[T_SENTENCE__ALIGNED_SOURCE_SENTENCES][0]!==sSentence)
1044 this[T_SENTENCE__ALIGNED_SOURCE_SENTENCES].unshift(sSentence);
1045};
1046
1047TargetSentence.prototype.setText = function _setText(text, isFullText){
1048 this[T_SENTENCE__TEXT] = text;
1049 this[T_SENTENCE__PARTS] = undefined;
1050
1051 if (isFullText){
1052 this[T_SENTENCE__LAST_FULL_TEXT] = text.trim();
1053 }
1054 this.takeSnapshot(isFullText);
1055};
1056const REGEX_beginningWhitespaces = /^\s*/;
1057const REGEX_endingWhitespaces = /\s*$/;
1058TargetSentence.prototype.getParts = function _getParts(){
1059 if (!this[T_SENTENCE__PARTS])
1060 this[T_SENTENCE__PARTS] = splitTextIntoParts(this[T_SENTENCE__TEXT]);
1061 return this[T_SENTENCE__PARTS];
1062};
1063TargetSentence.prototype.getNormalizedText = function _getNormalizedText(){
1064 return this[T_SENTENCE__TEXT].replace(REGEX_collapseWhitspaces," ").trim(); //cleanup whitespaces whitespace formatting
1065};
1066TargetSentence.prototype.getNormalizedTextAndOffset = function _getNormalizedTextAndOffset(textOffset){
1067 let normalizedOffset = 0;
1068 var normalizedTexts = [];
1069 this.getParts().forEach(function(part, i){
1070 let normalizedText;
1071 if (part.isWhitespacePart){
1072 normalizedText = i==0 ? "" : " ";
1073 } else {
1074 normalizedText = part.text;
1075 }
1076 if (textOffset>=part.text.length){ // jump over this part
1077 normalizedOffset += normalizedText.length;
1078 textOffset -= part.text.length;
1079 }else if (textOffset>=0){
1080 normalizedOffset += Math.min(textOffset, normalizedText.length);
1081 textOffset -= part.text.length;
1082 }
1083 normalizedTexts.push(normalizedText);
1084 });
1085 return [normalizedTexts.join(""), normalizedOffset];
1086};
1087TargetSentence.prototype.getWhitespacesBefore = function _getWhitespacesBefore(){
1088 return this[T_SENTENCE__TEXT].match(REGEX_beginningWhitespaces)[0];
1089};
1090
1091TargetSentence.prototype.getWhitespacesAfter = function _getWhitespacesAfter(){
1092 return this[T_SENTENCE__TEXT].match(REGEX_endingWhitespaces)[0];
1093};
1094
1095TargetSentence.prototype.getText = function _getText(){
1096 return this[T_SENTENCE__TEXT];
1097};
1098
1099TargetSentence.prototype.takeSnapshot = function _takeSnapshot(isFullText){
1100 const text = this.getNormalizedText();
1101 const snapshots = this[T_SENTENCE__EDIT_SNAPSHOTS];
1102 if (!snapshots.length || text !== snapshots[snapshots.length-1].text || isFullText !== snapshots[snapshots.length-1].isFullText){
1103 snapshots.push({
1104 text: text,
1105 isFullText: isFullText
1106 });
1107 }
1108};
1109TargetSentence.prototype.getSnapshot = function _getSnapshot(nr){
1110 return this[T_SENTENCE__EDIT_SNAPSHOTS][nr < 0 ? this[T_SENTENCE__EDIT_SNAPSHOTS].length + nr : nr]
1111}
1112TargetSentence.prototype.getFullSnapshot = function _getSnapshot(nr){
1113 const fullSnapshots = this[T_SENTENCE__EDIT_SNAPSHOTS].filter(snapshot => snapshot.isFullText)
1114 return fullSnapshots[nr < 0 ? fullSnapshots.length + nr : nr]
1115}
1116
1117/**
1118 * @param {string} text
1119 * @return {LMT_TargetSentence} a new TargetSentence
1120 */
1121function createTargetSentence(text){
1122 return dev_sealProxy(new TargetSentence(text));
1123}
1124
1125// ---
1126
1127// = == =============================== Errors ================================ == =
1128const LMT_ERROR_TYPE__RPC_ERROR = 'ERROR_TYPE__RPC_ERROR';
1129const LMT_ERROR_TYPE__LINE_TOO_LONG_TO_SPLIT = 'LMT_ERROR_TYPE__LINE_TOO_LONG_TO_SPLIT';
1130
1131function createLMTError(type){
1132 return {
1133 errorType: type
1134 };
1135}
1136
1137// = == =============================== Data functions ================================ == =
1138// Please try not to use any DOM-Elements in the block below (separate data from view)
1139
1140// some constants (for better autocompletion in the IDE...)
1141const JOB_KIND__DEFAULT = 'default';
1142const JOB_KIND__AUTOCOMPLETION = 'autocompletion';
1143const JOB_KIND__ALTERNATIVES_AT_POSITION = 'alternatives_at_position';
1144const RECORD__POSTPROCESSED_SENTENCE = 'postprocessed_sentence';
1145const TRANSLATION_RESULT__TRANSLATIONS = 'translations';
1146const TRANSLATION_RESULT__SOURCE_LANG = 'source_lang';
1147const TRANSLATION_RESULT__TARGET_LANG = 'target_lang';
1148
1149
1150// = == =============== Feature: translationBackend =============== == =
1151
1152webTranslator.features.define('translationBackend',
1153 ['eventHandling', 'config'],
1154 (eventHandling, config) => {
1155
1156 const dbg = dlPageState.devMode;
1157 const TAG = "[translationBackend]";
1158
1159 const translationBackendIsScheduler = U.value(true)
1160 const backendURL_translation = U.value()
1161 const backendURL_sentenceSplitting = U.value()
1162 const doFinalizeJobRequestParameters = U.createMultiProcedure()
1163 const doFinalizeSentenceSplittingRequestParameters = U.createMultiProcedure()
1164
1165 // configuration
1166 {
1167 let backendUrl;
1168 if (queryVars["backend"]){
1169 backendUrl = queryVars["backend"] // e.g.: "https://www.deepl.com/jsonrpc";
1170 }
1171 else if (config[CONFIG__IS_PRO]){
1172 if (location.href.match(/test\.deepl\.com/) || location.href.match(/local.*\.deepl\.com/)){
1173 backendUrl = "https://api-test.deepl.com/jsonrpc";
1174 } else if (location.href.match(/dev\.deepl\.com/)){
1175 backendUrl = "https://api-dev.deepl.com/jsonrpc";
1176 } else if (location.href.match(/beta\.deepl\.com/)){
1177 backendUrl = "https://beta.deepl.com/jsonrpc";
1178 } else { // live
1179 backendUrl = "https://api.deepl.com/jsonrpc";
1180 }
1181 }
1182 else if (location.href.match(/\/\/dev\.deepl\.com/) ||
1183 location.href.match(/test\.deepl\.com/)) {
1184 backendUrl = "../jsonrpc";
1185 }
1186 else {
1187 backendUrl = "https://www2.deepl.com/jsonrpc";
1188 }
1189
1190 if (queryVars['tBackend']){ // explicit translation backend http://local.deepl.com:8082/PHP/translator.php?tBackend=heinzel95:6007
1191 let url = queryVars['tBackend'];
1192 if (!url.startsWith('http') && !url.startsWith('/'))
1193 url = 'http://' + url;
1194 backendURL_translation(url);
1195 translationBackendIsScheduler(false);
1196 }
1197 else {
1198 backendURL_translation(backendUrl);
1199 }
1200
1201 if (queryVars['sBackend']){ // explicit sentenceSplitting backend http://local.deepl.com:8082/PHP/translator.php?sBackend=heinzel95:6007
1202 let url = queryVars['sBackend'];
1203 if (!url.startsWith('http') && !url.startsWith('/'))
1204 url = 'http://' + url;
1205 backendURL_sentenceSplitting(url);
1206 }
1207 else {
1208 backendURL_sentenceSplitting(backendUrl);
1209 }
1210 }
1211
1212 const lmtBackends = {
1213 'translation': U.computedValue(
1214 [backendURL_translation, translationBackendIsScheduler],
1215 (url, translationBackendIsScheduler) => createBackend(url, translationBackendIsScheduler)
1216 ),
1217 'sentenceSplitting': U.computedValue(
1218 [backendURL_sentenceSplitting],
1219 (url) => createBackend(url, undefined)
1220 ),
1221 }
1222
1223 // simulate blocked user
1224 queryVars['dev_block'] && U.withValue(
1225 lmtBackends['translation'],
1226 lmtBackend => lmtBackend.rpcConfig = {queryParams:{'x-simulate-too-many-requests':'1'}})
1227
1228 // debug output
1229 dbg && U.withValue(
1230 [backendURL_translation, translationBackendIsScheduler, backendURL_sentenceSplitting],
1231 (backendURL_translation, translationBackendIsScheduler, backendURL_sentenceSplitting) => {
1232 console.log(TAG, `\n backendURL_translation: ${backendURL_translation} (${translationBackendIsScheduler?"scheduler": "direct"}) \n backendURL_sentenceSplitting: ${backendURL_sentenceSplitting}`);
1233 });
1234
1235 function createBackend(url, isScheduler){
1236 const lmtBackend = new LMT_Backend({
1237 url,
1238 isScheduler,
1239 doFinalizeJobRequestParameters,
1240 doFinalizeSentenceSplittingRequestParameters,
1241 })
1242 lmtBackend.onRequestSucceeded.push( rqType => eventHandling.notify(eventHandling.ON.ON__RPC_CALL_SUCCESSFULL, rqType));
1243 lmtBackend.onRequestFailed.push( rpcError => eventHandling.notify(eventHandling.ON.ON__RPC_CALL_FAILED, "translation", rpcError));
1244 return lmtBackend
1245 }
1246
1247 return {
1248 lmtBackends,
1249 doFinalizeJobRequestParameters,
1250 doFinalizeSentenceSplittingRequestParameters,
1251 backendURL_translation,
1252 backendURL_sentenceSplitting,
1253 sendTranslationJobs(...params){
1254 return lmtBackends['translation']().sendTranslationJobs(...params)
1255 },
1256 sendTranslationJobs_batched(...params){
1257 return lmtBackends['translation']().sendTranslationJobs_batched(...params)
1258 },
1259 requestSentenceSplitting(...params){
1260 return lmtBackends['sentenceSplitting']().requestSentenceSplitting(...params)
1261 },
1262 sendAdditionalData(...params){
1263 return lmtBackends['translation']().sendAdditionalData(...params)
1264 },
1265 translationProvidesLangDetection: U.computedValue(translationBackendIsScheduler, v => v),
1266 }
1267});
1268webTranslator.translationBackend = webTranslator.features.get('translationBackend');
1269
1270// = == =============== end: translationBackend =============== == =
1271
1272
1273// = == =============== translatorStats =============== == =
1274webTranslator.features.define('translatorStats',
1275 ['translationBackend'],
1276 ({lmtBackends}) => {
1277
1278 let translationNumberSymbolsForAnalytics = 0
1279 let translationNumberRequestsForAnalytics = 0
1280 U.withValue(lmtBackends['translation'], (lmtBackend) => {
1281 lmtBackend.onTranslationsReceived.push( function countContact(result, translations, jobs, langConfig) {
1282 if (typeof result.timestamp!="undefined") {
1283 dlStats.countStats("f",function (x) { return x==0? result.timestamp : x; },0);
1284 }
1285 if (typeof result.date!="undefined") {
1286 var gs = dlStats.getStats("g","");
1287 if (gs()!=result.date) {
1288 gs(result.date);
1289 dlStats.countStats("h",function (x) { return x+1; },0);
1290 dlStats.countEvent("translator_day",1);
1291 }
1292 }
1293
1294 let numSymbols = 0;
1295 translations.forEach(function (t) {
1296 var beams = t["beams"];
1297 if (Array.isArray(beams) && beams.length > 0) {
1298 numSymbols += beams[0].num_symbols;
1299 }
1300 });
1301 const lp = result["source_lang"] + langConfig["target_lang"];
1302
1303 // disable logging for edit requests, to save analytics hits
1304 const kind = (Array.isArray(jobs) && jobs.length > 0) ? jobs[0].kind: '';
1305 if (kind!='default') {
1306 if (kind=="alternatives_at_position") {
1307 dlStats.countStats("d", function(x) { return x+1; }, 0);
1308 dlStats.countEvent("target_alternatives_open",1);
1309 }
1310 } else {
1311 dlStats.countStats("a", function(x) { return x+numSymbols; }, 0);
1312 if (lp.length==4) {
1313 dlStats.countStats("b", function (x) {
1314 x[lp] = numSymbols + ((typeof x[lp] == "undefined") ? 0 : x[lp]);
1315 return JSON.parse(JSON.stringify(x));
1316 }, {});
1317 }
1318 translationNumberSymbolsForAnalytics += numSymbols;
1319 translationNumberRequestsForAnalytics += 1;
1320
1321 window.ga && U.setTimeout_consolidated("analytics_translation", function() {
1322 false && window.ga('send', 'event', 'translation', 'symbols', 'default', translationNumberSymbolsForAnalytics); // disabled
1323 window.ga('send', 'event', 'translation', 'requests', 'default', translationNumberRequestsForAnalytics);
1324 translationNumberSymbolsForAnalytics = 0;
1325 translationNumberRequestsForAnalytics = 0;
1326 },60000);
1327 }
1328 });
1329 })
1330})
1331// = == =============== end: translatorStats =============== == =
1332
1333
1334// = == =============== updateSourceLanguageAndSentences =============== == =
1335/**
1336 * If the source text changes, request a sentente splitting, and updates the source sentences accordingly (and update the source language if necessary)
1337 */
1338webTranslator.features.define('updateSourceLanguageAndSentences', ['translationBackend', 'eventHandling', 'webTranslatorCore', 'domElements', 'stateManagement'],
1339 (translationBackend, {on, ON, notify}, webTranslatorCore, domElements, stateManagement) => {
1340
1341 const $sourceEdit = domElements.$sourceEdit;
1342
1343 /**
1344 * Called after the source text changed. Match a text with the state's source sentences and detect which part have changed and require
1345 * a new sentence splitting.
1346 * - (heuristic) if the change is close to the beginning of the text, the request can be extended to the front as the result may be use
1347 * to change the detected language
1348 * - (heuristic) if request is too short, add more lines to improve language detection stability
1349 * - if a line is too long, LMT_ERROR_TYPE__LINE_TOO_LONG_TO_SPLIT is returned.
1350 * @param {LMT_State} state
1351 * @param {string} rawSourceText
1352 * @returns {Object}
1353 * {
1354 * unchangedSplittedParagraphsPrefix: Array<string>,
1355 * changedCleanedParagraphs: Array<string>,
1356 * unchangedSplittedParagraphsPostfix: Array<string>
1357 * } or
1358 * LMT_ERROR_TYPE__LINE_TOO_LONG_TO_SPLIT
1359 */
1360 function doSelectParagraphsRequiringNewSentenceSplitting(state, rawSourceText){
1361 const TAG = "[doSelectParagraphsRequiringNewSentenceSplitting]";
1362 const dbg = false;
1363 let tooLongLineDetected = false;
1364 const cleanedParagraphs = [];
1365 rawSourceText.split(/(?:\s*\n)+\s*/g).forEach(function(rawText){
1366 const cleanedText = rawText.replace(REGEX_collapseWhitspaces," ").trim(); //cleanup whitespaces whitespace formatting
1367 if (cleanedText.length>conf_maxSourceLineLength)
1368 tooLongLineDetected = true;
1369
1370 if (cleanedText.length)
1371 cleanedParagraphs.push(cleanedText);
1372 });
1373
1374 if (tooLongLineDetected){
1375 return LMT_ERROR_TYPE__LINE_TOO_LONG_TO_SPLIT;
1376 }
1377
1378 let changedCleanedParagraphs = []; // [ paragraph_x, paragraph_x1, ... ]
1379 const unchangedSplittedParagraphsPrefix = []; // [ [sentence_0, sentence_1, ..], ... ]
1380 const unchangedSplittedParagraphsPostfix = []; // [ ... [..., sentence_n-2, sentence_n-1] ]
1381
1382 // identify which part has changed
1383 dbg && console.log(TAG, rawSourceText, state[STATE__SOURCE_SENTENCES]);
1384 let sSentenceCursor = 0;
1385 let paragraphCursor = 0;
1386 let unchangedPrefixLength = 0;
1387
1388 // find matching prefix
1389 while (state[STATE__SOURCE_SENTENCES][sSentenceCursor] && cleanedParagraphs[paragraphCursor]!==undefined){
1390 let consumableParagraphText = cleanedParagraphs[paragraphCursor];
1391 const sentences = [];
1392 while (consumableParagraphText.length){
1393 const sSentence = state[STATE__SOURCE_SENTENCES][sSentenceCursor];
1394 if (!sSentence)
1395 break;
1396 const normalizedSentenceText = sSentence.getNormalizedText();
1397 sentences.push(normalizedSentenceText);
1398 if (consumableParagraphText.startsWith(normalizedSentenceText)){
1399 dbg && console.log(TAG, "Sentence matched");
1400 consumableParagraphText = consumableParagraphText.substr(normalizedSentenceText.length).trim();
1401 ++sSentenceCursor;
1402 }
1403 else{
1404 break;
1405 }
1406 }
1407 if (!consumableParagraphText.length){
1408 dbg && console.log(TAG, "paragraph consumed");
1409 unchangedPrefixLength += cleanedParagraphs[paragraphCursor].length;
1410 unchangedSplittedParagraphsPrefix.push(sentences);
1411 ++paragraphCursor;
1412 } else {
1413 dbg && console.log(TAG, "change found", sSentenceCursor, paragraphCursor);
1414 break;
1415 }
1416 }
1417 const MINIMAL_LANGUAGE_DETECTION_TEXT_LENGTH = 500;
1418 if (unchangedPrefixLength>0 && unchangedPrefixLength<MINIMAL_LANGUAGE_DETECTION_TEXT_LENGTH){
1419 dbg && console.log(TAG, "Changes too close to beginning (unsafe language detection); moving to front");
1420 unchangedPrefixLength = 0;
1421 unchangedSplittedParagraphsPrefix.splice(0);
1422 }
1423
1424 // find matching postfix
1425 let sSentenceCursor_back = state[STATE__SOURCE_SENTENCES].length-1;
1426 let paragraphCursor_back = cleanedParagraphs.length-1;
1427
1428 while (state[STATE__SOURCE_SENTENCES][sSentenceCursor_back] && cleanedParagraphs[paragraphCursor]!==undefined &&
1429 sSentenceCursor_back>sSentenceCursor && paragraphCursor_back>paragraphCursor){
1430
1431 let consumableParagraphText = cleanedParagraphs[paragraphCursor_back];
1432 const sentences = [];
1433 while (consumableParagraphText.length){
1434 const sSentence = state[STATE__SOURCE_SENTENCES][sSentenceCursor_back];
1435 if (!sSentence)
1436 break;
1437 const normalizedSentenceText = sSentence.getNormalizedText();
1438 sentences.unshift(normalizedSentenceText);
1439 if (consumableParagraphText.endsWith(normalizedSentenceText)){
1440 // dbg && console.log(TAG, "Sentence matched");
1441 consumableParagraphText = consumableParagraphText.substr(0, consumableParagraphText.length - normalizedSentenceText.length).trim();
1442 --sSentenceCursor_back;
1443 }else{
1444 break;
1445 }
1446 }
1447 if (!consumableParagraphText.length){
1448 // dbg && console.log(TAG, "paragraph consumed");
1449 unchangedSplittedParagraphsPostfix.unshift(sentences);
1450 --paragraphCursor_back;
1451 } else {
1452 // dbg && console.log(TAG, "change found", sSentenceCursor_back, paragraphCursor_back);
1453 break;
1454 }
1455 }
1456 changedCleanedParagraphs = cleanedParagraphs.slice(unchangedSplittedParagraphsPrefix.length,
1457 cleanedParagraphs.length-unchangedSplittedParagraphsPostfix.length);
1458
1459 // increase size to improve language detection stability
1460 let changedLength = 0;
1461 changedCleanedParagraphs.forEach(function(p){
1462 changedLength += p.length;
1463 });
1464 if (changedLength>0){
1465 let frontBackFlip = 0;
1466 while (changedLength<MINIMAL_LANGUAGE_DETECTION_TEXT_LENGTH){
1467 dbg && console.log(TAG, "increase length");
1468 frontBackFlip = 1-frontBackFlip;
1469 if (frontBackFlip){
1470 if (unchangedSplittedParagraphsPrefix.length){
1471 const p = unchangedSplittedParagraphsPrefix.pop().join(" ");
1472 changedCleanedParagraphs.unshift(p);
1473 changedLength += p.length;
1474 }
1475 else if(unchangedSplittedParagraphsPostfix.length) {
1476 const p = unchangedSplittedParagraphsPostfix.shift().join(" ");
1477 changedCleanedParagraphs.push(p);
1478 changedLength += p.length;
1479 }
1480 else {
1481 break;
1482 }
1483 }else {
1484 if(unchangedSplittedParagraphsPostfix.length) {
1485 const p = unchangedSplittedParagraphsPostfix.shift().join(" ");
1486 changedCleanedParagraphs.push(p);
1487 changedLength += p.length;
1488 }
1489 else if (unchangedSplittedParagraphsPrefix.length){
1490 const p = unchangedSplittedParagraphsPrefix.pop().join(" ");
1491 changedCleanedParagraphs.unshift(p);
1492 changedLength += p.length;
1493 }
1494 else {
1495 break;
1496 }
1497 }
1498 }
1499 }
1500 dbg && console.log(TAG, "changed length", changedLength);
1501 dbg && console.log(TAG, "prefix", unchangedSplittedParagraphsPrefix);
1502 dbg && console.log(TAG, "changed", changedCleanedParagraphs);
1503 dbg && console.log(TAG, "postfix", unchangedSplittedParagraphsPostfix);
1504
1505 return {
1506 unchangedSplittedParagraphsPrefix: unchangedSplittedParagraphsPrefix,
1507 changedCleanedParagraphs: changedCleanedParagraphs,
1508 unchangedSplittedParagraphsPostfix: unchangedSplittedParagraphsPostfix
1509 };
1510 }
1511
1512 /**
1513 * Update the state's source data structure:
1514 * - update whites spaces before and after existing sentences
1515 * - create missing entries in the translation registry.
1516 * - create new source sentence objects
1517 *
1518 * @param {LMT_State} state
1519 * @param {Array<string>} sentenceTexts
1520 * @param {string} rawSourceText
1521 */
1522 function doUpdateSourceSentences(state, sentenceTexts, rawSourceText){
1523 function getSourceText(sentence){
1524 return sentence && sentence[S_SENTENCE__TRANSLATION] && sentence[S_SENTENCE__TRANSLATION][TRANSLATION__SOURCE_TEXT];
1525 }
1526 const dbg = false, TAG = '[doUpdateSourceSentences]';
1527 dbg && console.log(TAG, "ON.ON__NEW_SOURCE_SENTENCE_SPLITTING_RECEIVED");
1528
1529 // update sourceSentences.
1530 // console.log(sentenceTexts);
1531 const sourceSentences = state[STATE__SOURCE_SENTENCES];
1532 const remainingSSentences = sourceSentences.splice(0); // simply replace all existing sentence objects
1533
1534 //log(TAG, "Num SentenceTexts=", sentenceTexts.length, " remainingSSentences=",remainingSSentences.length);
1535 if (sentenceTexts) {
1536 const remainingSentenceTexts = sentenceTexts.slice(0);
1537 let remainingRawText = rawSourceText;
1538
1539 // consume unchanged sentences from the front (prevSentences, sentenceTexts,originalSourceText); update whitespaces
1540 while (remainingSentenceTexts.length && remainingSSentences.length &&
1541 getSourceText(remainingSSentences[0])===remainingSentenceTexts[0]){
1542 dbg && console.log(TAG, "FRONT");
1543 const sSentence = remainingSSentences.shift()
1544 sourceSentences.push(sSentence);
1545 const sentenceText = remainingSentenceTexts.shift();
1546 const whitespacesBefore = remainingRawText.match(REGEX_sentenceBeginningWhitespaces)[0];
1547
1548 sSentence.updateWhitespacesBefore(whitespacesBefore);
1549 if (whitespacesBefore.length)
1550 remainingRawText = remainingRawText.substr(whitespacesBefore.length);
1551
1552 if (remainingRawText.startsWith(sentenceText)){ // should be the default case
1553 sSentence[S_SENTENCE__RAW_TRIMMED_TEXT] = sentenceText;
1554 remainingRawText = remainingRawText.substr(sentenceText.length);
1555 }
1556 else{
1557 // the raw text of the sentence and the text returned for the sentence by the splitting differ
1558 // hopefully this is only due to different whitespaces...
1559 const condendesSentenceText = sentenceText.replace(/\s/g,"");
1560 const consumingRegex = new RegExp("^(\\s*\\S){" + condendesSentenceText.length + "}"); // consume the required number of chars while ignoring any whitespaces
1561 const m = remainingRawText.match(consumingRegex);
1562 if (m){
1563 //log(TAG, "Using heuristic to consume sentence");
1564 sSentence[S_SENTENCE__RAW_TRIMMED_TEXT] = m[0].trim();
1565
1566 remainingRawText = remainingRawText.substr(m[0].length);
1567 }
1568 else{
1569 sSentence[S_SENTENCE__RAW_TRIMMED_TEXT] = sentenceText; // better than nothing
1570
1571 console.warn(TAG, "Could not consume source sentence text!" );
1572 console.log("["+sentenceText+"]");
1573 console.log("["+remainingRawText+"]");
1574 // Todo: (claudius) If this occurs, find a way to revoker.
1575 }
1576 }
1577 {
1578 const m = remainingRawText.match(REGEX_sentenceEndingWhitespaces);
1579 if (m && m[0].length){
1580 sSentence.updateWhitespacesAfter(m[0]);
1581 remainingRawText = remainingRawText.substr(m[0].length);
1582 }
1583 else {
1584 sSentence.updateWhitespacesAfter("");
1585 }
1586 }
1587 dbg && console.log(TAG, "consume front");
1588 }
1589 // dbg && console.log(TAG, "", "["+getSourceText(remainingSSentences[0])+"]","["+remainingSentenceTexts[0]+"]");
1590
1591 // consume unchanged sentences from the back; update whitespaces
1592 const sSentencePostfix = [];
1593 while (remainingSentenceTexts.length && remainingSSentences.length &&
1594 getSourceText(remainingSSentences[remainingSSentences.length-1])===remainingSentenceTexts[remainingSentenceTexts.length-1]){
1595 //log(TAG, "BACK");
1596 const sSentence = remainingSSentences.pop();
1597 const sSentenceText = remainingSentenceTexts.pop();
1598 sSentencePostfix.unshift(sSentence);
1599 const whitespacesAfter = remainingRawText.match(REGEX_sentenceEndingWhitespacesBack)[0];
1600 sSentence.updateWhitespacesAfter(whitespacesAfter);
1601 if (whitespacesAfter.length)
1602 remainingRawText = remainingRawText.substr(0, remainingRawText.length-whitespacesAfter.length);
1603
1604 if (remainingRawText.endsWith(sSentenceText)){ // should be the default case
1605 sSentence[S_SENTENCE__RAW_TRIMMED_TEXT] = sSentenceText;
1606 remainingRawText = remainingRawText.substr(0, remainingRawText.length-sSentenceText.length);
1607 }
1608 else{
1609 // the raw text of the sentence and the text returned for the sentence by the splitting differ
1610 // hopefully this is only due to different whitespaces...
1611 const condendesSentenceText = sSentenceText.replace(/\s/g,"");
1612 const consumingRegex = new RegExp("(\\S(\\s*\\S){" + (condendesSentenceText.length-1) + "})$"); // consume the required number of chars while ignoring any whitespaces
1613 const m = remainingRawText.match(consumingRegex);
1614 if (m){
1615 sSentence[S_SENTENCE__RAW_TRIMMED_TEXT] = m[0].trim();
1616 remainingRawText = remainingRawText.substr(0, remainingRawText.length - m[0].length);
1617 }
1618 else{
1619 console.warn(TAG, "(back) Could not consume source sentence text!" );
1620 console.log("["+sSentenceText+"]");
1621 console.log("["+remainingRawText+"]");
1622 // Todo: (claudius) If this occurs, find a way to revoker.
1623 }
1624 }
1625 const m = remainingRawText.match(REGEX_sentenceBeginningWhitespacesBack);
1626 if (m){
1627 sSentence.updateWhitespacesBefore(m[1]);
1628 remainingRawText = remainingRawText.substr(0,remainingRawText. length - m[1].length);
1629 }
1630 else {
1631 sSentence.updateWhitespacesBefore("");
1632 }
1633 //log(TAG, "", "["+sSentence[S_SENTENCE__WHITESPACES_BEFORE]+"]["+sSentenceText+"]["+sSentence[S_SENTENCE__WHITESPACES_BEFORE]+"]");
1634 dbg && console.log(TAG, "consume back");
1635
1636 }
1637
1638 const newSSentencesAndTexts = [];
1639
1640 // update remaining
1641 remainingSentenceTexts.forEach(function(sSentenceText){
1642 dbg && console.log(TAG, "changed/new");
1643 var newSSentence = createSourceSentence();
1644 newSSentencesAndTexts.push([newSSentence, sSentenceText]);
1645
1646 if (remainingSSentences.length){ // replaces old sentence
1647 var replacedSSentence = remainingSSentences.shift();
1648
1649 // copy all relevant possible prior translations (to be able to always show the latest translation even if the sentence has changed afterwards)
1650 var prevTranslationsObjects = replacedSSentence[S_SENTENCE__PREVIOUS_TRANSLATIONS] ? replacedSSentence[S_SENTENCE__PREVIOUS_TRANSLATIONS].slice(0) : [];
1651 prevTranslationsObjects.unshift(replacedSSentence[S_SENTENCE__TRANSLATION]);
1652 var prevTranslationsObjects_truncated = [];
1653
1654 while (prevTranslationsObjects.length){
1655 var prevTranslationObject = prevTranslationsObjects.shift();
1656 prevTranslationsObjects_truncated.push(prevTranslationObject);
1657 if (prevTranslationObject[TRANSLATION__INITIAL_TRANSLATION_TEXT]) // truncate... we never look beyond this point
1658 break;
1659 }
1660
1661 newSSentence[S_SENTENCE__PREVIOUS_TRANSLATIONS] = prevTranslationsObjects_truncated; // store previous translation to be able to show the last translation until the new one arrives.
1662 }
1663
1664 // detect and set whitespaces
1665 const whitespacesBefore = remainingRawText.match(REGEX_sentenceBeginningWhitespaces)[0];
1666 newSSentence.updateWhitespacesBefore(whitespacesBefore);
1667 if (whitespacesBefore.length)
1668 remainingRawText = remainingRawText.substr(whitespacesBefore.length);
1669 if (remainingRawText.startsWith(sSentenceText)){ // should be the default case
1670 newSSentence[S_SENTENCE__RAW_TRIMMED_TEXT] = sSentenceText;
1671
1672 remainingRawText = remainingRawText.substr(sSentenceText.length);
1673 }else{
1674 // the raw text of the sentence and the text returned for the sentence by the splitting differ
1675 // hopefully this is only due to different whitespaces...
1676 const condendesSentenceText = sSentenceText.replace(/\s/g,"");
1677 const consumingRegex = new RegExp("^(\\s*\\S){" + condendesSentenceText.length + "}"); // consume the required number of chars while ignoring any whitespace
1678 const m = remainingRawText.match(consumingRegex);
1679 if (m){
1680 //log(TAG, "Using heuristic to consume sentence");
1681 newSSentence[S_SENTENCE__RAW_TRIMMED_TEXT] = m[0].trim();
1682
1683 remainingRawText = remainingRawText.substr(m[0].length);
1684 }
1685 else{
1686 console.warn(TAG, "Could not consume source sentence text!" );
1687 console.log("[" + sSentenceText + "]");
1688 console.log("[" + remainingRawText + "]");
1689 // Todo: (claudius) If this occurs, find a way to revoker.
1690 }
1691 }
1692 const m = remainingRawText.match(REGEX_sentenceEndingWhitespaces);
1693 if (m){
1694 newSSentence.updateWhitespacesAfter(m[0]);
1695 remainingRawText = remainingRawText.substr(m[0].length);
1696 }
1697 else {
1698 newSSentence.updateWhitespacesAfter("");
1699 }
1700 sourceSentences.push(newSSentence);
1701
1702 dbg && console.log(TAG, "New sentence: ", "[" + newSSentence[S_SENTENCE__WHITESPACES_BEFORE] + "]", "[" + newSSentence[S_SENTENCE__RAW_TRIMMED_TEXT] + "]", "[" + newSSentence[S_SENTENCE__WHITESPACES_AFTER] + "]" );
1703 });
1704
1705 while (sSentencePostfix.length)
1706 sourceSentences.push(sSentencePostfix.shift());
1707
1708 // set neighbor information for context determination
1709 state.isContextEnabled() && sourceSentences.forEach(function(sSentence, i){
1710 sSentence[S_SENTENCE__PREV_SENTENCE] = sourceSentences[i-1];
1711 sSentence[S_SENTENCE__NEXT_SENTENCE] = sourceSentences[i+1];
1712 });
1713
1714 // init new translations
1715 newSSentencesAndTexts.forEach(function(newSSentence_sSentenceText){
1716 var newSSentence = newSSentence_sSentenceText[0];
1717 var sSentenceText = newSSentence_sSentenceText[1];
1718
1719 let translation_key;
1720 if (state.isContextEnabled()){
1721 var context = doGetSourceContext(newSSentence);
1722 translation_key = context.prefix.join('/') + "//" + sSentenceText + "//" + context.postfix.join('/');
1723 }
1724 else {
1725 translation_key = sSentenceText;
1726 }
1727 var translation = state[STATE__TRANSLATION_REGISTRY][translation_key];
1728 if (translation===undefined){
1729 translation = createTranslation(sSentenceText); // ???? store context?
1730 state[STATE__TRANSLATION_REGISTRY][translation_key] = translation;
1731 dbg && console.log(TAG, "create new translation ["+translation_key+"]");
1732 }else{
1733 dbg && console.log(TAG, "Found existing translation ["+translation_key+"]");
1734 }
1735 newSSentence[S_SENTENCE__TRANSLATION] = translation;
1736
1737 });
1738
1739 }
1740 }
1741
1742 // eslint-disable-next-line no-control-regex
1743 const REGEX_invalidCharacter = /[\x00-\x08\x0B-\x1f\x7F\xAD]/g;
1744
1745 /**
1746 * After the source text changed -> request sentence splitting
1747 * - if a line is too long for sentence splitting, split it and try again
1748 * - can update the current source language based on the detected source language
1749 *
1750 * @listens LMT_Event ON.ON__SOURCE_TEXT_CHANGED
1751 * @fires LMT_Event --> ON.ON__SOURCE_SENTENCES_MAY_REQUIRE_UPDATE (requestContext)
1752 */
1753 on(ON.ON__SOURCE_TEXT_CHANGED, function _requestNewSourceSplitting(rqContext){
1754 notify(ON.ON__SOURCE_SENTENCES_MAY_REQUIRE_UPDATE, rqContext);
1755 });
1756
1757 /**
1758 * Afte the source text changed or when low quality typing request should get reviesed -> request sentence splitting
1759 * - if a line is too long for sentence splitting, split it and fire ON__SOURCE_TEXT_CHANGED
1760 * - can update the current source language based on the detected source language
1761 *
1762 * @listens LMT_Event ON.ON__SOURCE_TEXT_CHANGED
1763 * @fires LMT_Event --> ON.ON__NEW_SOURCE_SENTENCE_SPLITTING_RECEIVED (rqContext, trimmedSentencens, sourceText)
1764 * is fired when the new sentence splitting arrived (even an empty one)
1765 */
1766 on(ON.ON__SOURCE_SENTENCES_MAY_REQUIRE_UPDATE, function _requestNewSourceSplitting(rqContext){
1767 const dbg = false;
1768 const TAG = '[splitting]';
1769 var state = rqContext.state;
1770 //console.log("ON.ON__SOURCE_TEXT_CHANGED", new Error().stack);
1771
1772 var textInTextarea = $sourceEdit.val();
1773 if (textInTextarea.match(REGEX_invalidCharacter)){ // remove invalid control characters
1774 textInTextarea = textInTextarea.replace(REGEX_invalidCharacter, "");
1775 dbg && console.log(TAG, "Invalid characters detected and removed.");
1776 $sourceEdit.val(textInTextarea);
1777 notify(ON.ON__SOURCE_TEXT_CHANGED, rqContext);
1778 return;
1779 }
1780
1781 notify(ON.ON__REQUEST_STARTED, "translateRequest");
1782 rqContext.onRequestEnded.push(function(){
1783 notify(ON.ON__REQUEST_ENDED, "translateRequest");
1784 });
1785
1786 const selectedSourceLang = webTranslatorCore.getSelectedSourceLang()
1787 const langConfig = {
1788 'lang_user_selected': selectedSourceLang,
1789 'user_preferred_langs': stateManagement.getPrioritizedSourceLangs(),
1790 };
1791 const selectedTargetLang = webTranslatorCore.getSelectedTargetLang();
1792 let mySourceText;
1793 U.scheduleNonConcurrentRequest("sentenceSplitting" + state.getId(), function(){
1794 return new Promise(function(success, failure){
1795 var rawSourceText = textInTextarea.substr(0, conf_maxNumCharacters);
1796 if (globalState[GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE]()){ // only use first paragraph
1797 var m = rawSourceText.match(/\s*.+(\n|$)/);
1798 if (m)
1799 rawSourceText = m[0];
1800 }
1801 mySourceText = rawSourceText;
1802 var splittingInfo = doSelectParagraphsRequiringNewSentenceSplitting(state, rawSourceText);
1803 if (splittingInfo===LMT_ERROR_TYPE__LINE_TOO_LONG_TO_SPLIT){
1804 failure(createLMTError(LMT_ERROR_TYPE__LINE_TOO_LONG_TO_SPLIT));
1805 } else {
1806 const forceSentenceSlitting = !translationBackend.translationProvidesLangDetection() || globalState[GSTATE__FORCE_SENTENCE_SPLITTING_REQUESTS_FOR_STABILITY]();
1807 translationBackend.requestSentenceSplitting(splittingInfo.changedCleanedParagraphs, langConfig, rqContext.initialTrigger, forceSentenceSlitting)
1808 .then(function(splittingResult){
1809
1810 splittingInfo.splittingResult = splittingResult;
1811 success(splittingInfo);
1812 }, failure);
1813 }
1814 });
1815 }, 5000).then(function _success(splittingInfo){
1816 dbg && console.log("[sentenceSplitting] response");
1817
1818 if (selectedSourceLang!=webTranslatorCore.getSelectedSourceLang() || selectedTargetLang!=webTranslatorCore.getSelectedTargetLang()) {
1819 log("IGNORED response from sentenceSplitting because selectedSourceLang, selectedTargetLang changed.");
1820 rqContext.onRequestEnded();
1821 return;
1822 }
1823 var splittingResult = splittingInfo.splittingResult;
1824 var listOfSentenceBlocks = splittingInfo.unchangedSplittedParagraphsPrefix.concat(splittingResult.splittedTextblocks, splittingInfo.unchangedSplittedParagraphsPostfix);
1825 var receivedSourceLang = splittingResult.detectedLang;
1826 var langIsConfident = splittingResult.langIsConfident;
1827
1828 //if (rqContext.initialTrigger==TRIGGER__SOURCE_LANG_CHANGED && webTranslatorCore.getSelectedSourceLang() !== "auto" && receivedSourceLang !== webTranslatorCore.getSelectedSourceLang()) {
1829 // dbg && console.log("[sentenceSplitting] IGNORING response from sentenceSplitting because source lang switched by server.");
1830 // rqContext.onRequestEnded();
1831 // return;
1832 //}
1833
1834 // Update ui properties
1835 if (receivedSourceLang!=="" && receivedSourceLang!==undefined) {
1836 log("[sentenceSplitting] pushPreferredLang("+receivedSourceLang+")");
1837 if (langIsConfident)
1838 webTranslatorCore.pushPreferredLang(receivedSourceLang);
1839 }
1840 let prefixLength = 0;
1841 splittingInfo.unchangedSplittedParagraphsPrefix.forEach(sentences => sentences.forEach(sentence => prefixLength += sentence.length));
1842 dbg && console.log(TAG,"unchangedSplittedParagraphsPrefixLength",prefixLength);
1843 if (receivedSourceLang && receivedSourceLang!==state.getSourceLang()){
1844
1845 if (prefixLength>500){
1846 log(TAG, "[sentenceSplitting] (Ignoring; Too much text in front) Different source lang detected ", state.getSourceLang(), "->", receivedSourceLang);
1847 }else{
1848 log(TAG, "[sentenceSplitting] Source lang changed ", state.getSourceLang(), "->", receivedSourceLang);
1849 stateManagement.changedSourceLangDetected(receivedSourceLang)
1850 }
1851 }
1852 var trimmedSentencens = [];
1853 listOfSentenceBlocks.forEach(function(sentences){
1854 sentences.forEach(function(sentence){
1855 trimmedSentencens.push(sentence.trim()); // although the data should already be trimmed, assure that it really is
1856 })
1857 });
1858 if (!webTranslatorCore.isActiveState(state)){
1859 rqContext.onRequestEnded();
1860 rqContext = new RequestContext(webTranslatorCore.getActiveState(), rqContext.initialTrigger); // is this actually safe?
1861 }
1862 rqContext.splitSentenceRequestUsed = !!splittingResult.splitSentenceRequestUsed;
1863 rqContext.sourceLangHasBeenDetermined = !!receivedSourceLang;
1864 notify(ON.ON__NEW_SOURCE_SENTENCE_SPLITTING_RECEIVED, rqContext, trimmedSentencens, mySourceText);
1865
1866 }, function _fail(cancel_or_lmtError){
1867 rqContext.onRequestEnded();
1868 if (cancel_or_lmtError===U.scheduleNonConcurrentRequest.FAILURE_REASON__CANCELED){
1869 dbg && console.log("[sentenceSplitting] CANCELED");
1870 }else { // Line to long for splitting result
1871 var lmtError = cancel_or_lmtError;
1872 if (lmtError.errorType === LMT_ERROR_TYPE__LINE_TOO_LONG_TO_SPLIT){
1873 console.warn(TAG, "Too long line detected");
1874 (function _splitTooLongLines(){
1875 var newLines = [];
1876 var PREFIX_LENGTH = (conf_maxSourceLineLength*0.75) << 0;
1877 var SPLITTERS = ["! ", "? ", ": ", "; ", ". ", ", ", '" ', " "];
1878 $sourceEdit.val().split("\n").forEach(function _processLine(line){
1879 for (;;){
1880 if (line.length<conf_maxSourceLineLength){
1881 newLines.push(line);
1882 break;
1883 } else {
1884 console.log("[lineSplit]", "Split long line", line.length);
1885 var _before = line.length;
1886 var splitFound = false;
1887 for (let i=0; i<SPLITTERS.length; ++i){
1888 var pos = line.indexOf(SPLITTERS[i],PREFIX_LENGTH);
1889 // console.log("["+SPLITTERS[i]+"]", pos);
1890 if (pos>=0 && pos < conf_maxSourceLineLength){
1891 newLines.push(line.substr(0, pos+1).trimEnd());
1892 line = " " + line.substr(pos + SPLITTERS[i].length).trimStart();
1893 splitFound = true;
1894 break;
1895 }
1896 }
1897 if (!splitFound){
1898 newLines.push(line.substr(0, PREFIX_LENGTH).trimEnd());
1899 line = " " + line.substr(PREFIX_LENGTH).trimStart();
1900 }
1901 if (line.length>=_before)
1902 throw new Error("Sanity check failed!");
1903 }
1904 }
1905 });
1906 $sourceEdit.val(newLines.join("\n"));
1907 notify(ON.ON__SOURCE_TEXT_CHANGED, new RequestContext(webTranslatorCore.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED)); // -> try again
1908 })();
1909 }
1910 else{
1911 notify(ON.ON__REQUEST_FAILED, "splitting", lmtError);
1912 console.error(lmtError);
1913 }
1914 }
1915 });
1916 });
1917
1918
1919 /* ? [ON.ON__SOURCE_TEXT_EMPTY]
1920 - reset the global state: ...
1921 */
1922 on(ON.ON__SOURCE_TEXT_EMPTY, function _resetGlobalState(){
1923 globalState[GSTATE__FORCE_SENTENCE_SPLITTING_REQUESTS_FOR_STABILITY](false);
1924 });
1925
1926
1927 /**
1928 * Update the source sentence data structures according to the list of sentence texts.
1929 *
1930 * @listens LMT_Event ON.ON__NEW_SOURCE_SENTENCE_SPLITTING_RECEIVED
1931 * @fires LMT_Event --> ON.ON__SOURCE_SENTENCES_CHANGED (rqContext)
1932 */
1933 on(ON.ON__NEW_SOURCE_SENTENCE_SPLITTING_RECEIVED, function _updateSourceSentencesData(rqContext, sentenceTexts, rawSourceText){
1934 doUpdateSourceSentences(rqContext.state, sentenceTexts, rawSourceText);
1935 notify(ON.ON__SOURCE_SENTENCES_CHANGED, rqContext);
1936 });
1937
1938 return {
1939
1940 };
1941});
1942// = == =============== end: updateSourceLanguageAndSentences =============== == =
1943
1944
1945
1946// = == =============== updateTargetSentences =============== == =
1947webTranslator.features.define('updateTargetSentences', [], () => {
1948
1949 const REGEX_matchBetweenSsentenceWhitespace = /^(\s*?\n)(\s*)|( ?)(\s+)$/;
1950 function splitBetweenSentenceWhitespaces(ws){
1951 var m = ws.match(REGEX_matchBetweenSsentenceWhitespace);
1952 if (!m){ // should not happen...
1953 return ["", ws];
1954 }else if (m[1]){ // [ \n][ ]
1955 return [m[1], m[2]];
1956 }else { // [ ][ ]
1957 return [m[3], m[4]];
1958 }
1959 }
1960
1961 /**
1962 * Updates the state's target data after the target text has been altered (by user edit, autocompletion, ...)
1963 * - update stored target text
1964 * - update target sentence texts and whitespaces (including edit-revisions)
1965 * - ...
1966 * - challenge: if more than one targte sentence changed, try to distribute the changed text to the exisiting
1967 * target sentences as good as possible
1968 *
1969 * todo: consume front (fast) Only based on complete sentences.
1970 *
1971 * @param {LMT_State} state - the state to be updated
1972 * @param {string} newTargetText - the new target text
1973 */
1974 function doRefreshTargetDataFromText(state, newTargetText){
1975 const dbg = false, TAG = '[doRefreshTargetDataFromText]';
1976 if (state[STATE__TARGET_TEXT]===newTargetText){
1977 dbg && console.log(TAG, "No changes.");
1978 return;
1979 }
1980
1981 var targetText = state[STATE__TARGET_TEXT] = newTargetText;
1982
1983 // split into parts
1984 var remainingParts = splitTextIntoParts(targetText);
1985 dbg && console.log(TAG, "parts ", remainingParts.slice(0));
1986
1987 var remainingTSentences = state[STATE__TARGET_SENTENCES].slice(0);
1988 let lastMatchedPrefixSentence;
1989 let lastMatchedPostfixSentence;
1990
1991 function updateTSentence(tSentence, texts, updateRevision){
1992 var oldSentenceParts = tSentence.getParts();
1993 if (oldSentenceParts.length){
1994 if (texts[0]!==oldSentenceParts[0].text && (oldSentenceParts[0].isWhitespacePart || texts[0].match(/^\s+$/)) ){
1995 dbg && console.log(TAG, "New prefix whitespaces [" + texts[0] + "] [" + oldSentenceParts[0].text + "]");
1996 tSentence[T_SENTENCE__WHITESPACES_BEFORE_REV] = counter();
1997
1998 }
1999 var lastOldPart = oldSentenceParts[oldSentenceParts.length-1];
2000 var lastText = texts[texts.length-1];
2001 if (lastText!==lastOldPart.text && (lastOldPart.isWhitespacePart || lastText.match(/^\s+$/))){
2002 dbg && console.log(TAG, "New postfix whitespaces [" + texts[texts.length-1] + "]");
2003 tSentence[T_SENTENCE__WHITESPACES_AFTER_REV] = counter();
2004 }
2005
2006 }
2007 tSentence.setText(texts.join(""), false);
2008 updateRevision && (tSentence[T_SENTENCE__EDIT_REV] = counter());
2009 }
2010
2011 // consume front
2012 dbg && console.log(TAG, "<<<< ");
2013 { // consume front parts
2014 var sentence = remainingTSentences[0];
2015 while (sentence){
2016 var sentenceParts = sentence.getParts();
2017 let partCursor = 0;
2018 let sentencePartCursor = 0;
2019 var part, sentencePart;
2020 var sentenceMatched = false;
2021 for (;;){
2022 sentencePart = sentenceParts[sentencePartCursor];
2023 while (sentencePart && sentencePart.isWhitespacePart) // skip whitespace parts
2024 sentencePart = sentenceParts[++sentencePartCursor];
2025
2026 part = remainingParts[partCursor];
2027 while (part && part.isWhitespacePart) // skip whitespace parts
2028 part = remainingParts[++partCursor];
2029
2030 if (!sentencePart){
2031 sentenceMatched = true;
2032 break;
2033 }
2034 if (!part)
2035 break;
2036 // dbg && console.log("Try to match: ["+part.text+"] [" + sentencePart.text + "]");
2037 if (part.text!==sentencePart.text){
2038 dbg && console.log(TAG, "["+part.text+"] [" + sentencePart.text + "] ", sentence);
2039 break;
2040 }
2041 ++partCursor;
2042 ++sentencePartCursor;
2043 }
2044 if (sentenceMatched){
2045 dbg && console.log(TAG, "Sentence matched (front)!",sentence);
2046 var theMatchedParts = remainingParts.splice(0, partCursor); // consume matched parts
2047
2048 if (theMatchedParts.length){
2049 var theLastMatchedPart = theMatchedParts[theMatchedParts.length-1];
2050 dbg && console.log(TAG, "The last matched part [" + theLastMatchedPart.text + "]", theLastMatchedPart.isWhitespacePart);
2051
2052 if (theLastMatchedPart.isWhitespacePart){
2053 var betweenSentenceWhitespaces = splitBetweenSentenceWhitespaces(theLastMatchedPart.text);
2054 dbg && console.log(TAG, "Between sentence whitespaces (1): ", "[" + betweenSentenceWhitespaces[0] + "]", "[" + betweenSentenceWhitespaces[1] + "]");
2055
2056 theLastMatchedPart.text = betweenSentenceWhitespaces[0];
2057
2058 if (betweenSentenceWhitespaces[1].length){
2059 remainingParts.unshift({
2060 text: betweenSentenceWhitespaces[1],
2061 isWhitespacePart: true
2062 });
2063 dbg && console.log(TAG, "Between sentence whitespaces (2): ", "[" + betweenSentenceWhitespaces[0] + "]", "[" + betweenSentenceWhitespaces[1] + "]");
2064
2065 }
2066 }
2067 }
2068 /*
2069 if (remainingParts.length>1 && remainingParts[0].isWhitespacePart && remainingParts[1].isWhitespacePart){
2070 theMatchedParts.push(remainingParts.shift());
2071 dbg && console.log(TAG, "add postfix whitespaces ");
2072 }else {
2073 dbg && console.log(TAG, "remaining parts", remainingParts);
2074 }*/
2075
2076 var texts = [];
2077 theMatchedParts.forEach(function(part){ texts.push(part.text); });
2078 updateTSentence(sentence, texts, false);
2079 // sentencesToUpdate.push([sentence, texts, false]);
2080
2081 //sentence.setText(texts.join(""), false);
2082 lastMatchedPrefixSentence = sentence;
2083 remainingTSentences.shift();
2084 sentence = remainingTSentences[0];
2085 dbg && console.log("----------");
2086 }else{
2087 // todo try previously filled up sentences...
2088 dbg && console.log(TAG, "No match.");
2089 break;
2090 }
2091 }
2092 }
2093 dbg && console.log("--------");
2094 { // consume back parts
2095 let sentence = remainingTSentences[remainingTSentences.length-1];
2096 while (sentence){
2097 const sentenceParts = sentence.getParts();
2098 let partCursor = remainingParts.length - 1;
2099 let sentencePartCursor = sentenceParts.length - 1;
2100 let part, sentencePart;
2101 let sentenceMatched = false;
2102 for (;;){
2103 sentencePart = sentenceParts[sentencePartCursor];
2104 while (sentencePart && sentencePart.isWhitespacePart) // skip whitespace parts
2105 sentencePart = sentenceParts[--sentencePartCursor];
2106
2107 part = remainingParts[partCursor];
2108 while (part && part.isWhitespacePart) // skip whitespace parts
2109 part = remainingParts[--partCursor];
2110
2111 if (!sentencePart){
2112 sentenceMatched = true;
2113 break;
2114 }
2115 if (!part)
2116 break;
2117 // dbg && console.log("Try to match: ["+part.text+"] [" + sentencePart.text + "]");
2118 if (part.text!==sentencePart.text){
2119 dbg && console.log(TAG, "Mismatch: ["+part.text+"] [" + sentencePart.text + "] ", sentence);
2120 break;
2121 }
2122 --partCursor;
2123 --sentencePartCursor;
2124 }
2125 if (sentenceMatched){
2126 dbg && console.log(TAG, "Sentence matched (back)!",sentence);
2127 const theMatchedParts = remainingParts.splice(partCursor + 1); // consume matched parts
2128
2129
2130 if (theMatchedParts.length){
2131 const theLastMatchedPart = theMatchedParts[0];
2132 if (theLastMatchedPart.isWhitespacePart){
2133 const betweenSentenceWhitespaces = splitBetweenSentenceWhitespaces(theLastMatchedPart.text);
2134 theLastMatchedPart.text = betweenSentenceWhitespaces[1];
2135 if (betweenSentenceWhitespaces[0].length){
2136 remainingParts.push({
2137 text: betweenSentenceWhitespaces[0],
2138 isWhitespacePart: true
2139 });
2140 dbg && console.log(TAG, "Between sentence whitespaces (2): ", "[" + betweenSentenceWhitespaces[0] + "]", "[" + betweenSentenceWhitespaces[1] + "]");
2141 }
2142 }
2143 }
2144 /*if (remainingParts.length>0 && remainingParts[remainingParts.length-1].isWhitespacePart){
2145 theMatchedParts.unshift(remainingParts.pop());
2146 dbg && console.log(TAG, "add postfix whitespaces (2) ");
2147 }*/
2148
2149 const texts = [];
2150 theMatchedParts.forEach(function(part){ texts.push(part.text); });
2151 updateTSentence(sentence, texts, false);
2152 //sentencesToUpdate.push([sentence, texts, false]);
2153 lastMatchedPostfixSentence = sentence;
2154 remainingTSentences.pop();
2155 sentence = remainingTSentences[remainingTSentences.length-1];
2156 dbg && console.log("----------");
2157 }else{
2158 dbg && console.log(TAG, "No match.");
2159 break;
2160 }
2161 }
2162 }
2163 dbg && state[STATE__TARGET_SENTENCES].forEach(function(tSentence,i){
2164 console.log(TAG, "tSentence ",i,"["+tSentence[T_SENTENCE__TEXT]+"]");
2165 });
2166
2167 // try to match the remaining sentences <-> parts
2168 if (remainingTSentences.length==1 && remainingParts.length){
2169 const tSentence = remainingTSentences.shift();
2170
2171 const texts = [];
2172 remainingParts.splice(0).forEach(function(part){
2173 texts.push(part.text);
2174 });
2175 //sentencesToUpdate.push([tSentence, texts, true]);
2176 updateTSentence(tSentence, texts, true);
2177 dbg && console.log(TAG, "Assign remaining parts to single sentence: ",tSentence);
2178
2179 // todo (later) detect if the remaining parts contain a new paragraph without corresponding source sentence -> create independent targetSentences
2180 }
2181 else if (!remainingTSentences.length && remainingParts.length){ // assign new parts to an existing sentence
2182 let assignToLeftOrRight = 0;
2183
2184 // let selectedSentence;
2185 if (!lastMatchedPrefixSentence && !lastMatchedPostfixSentence){
2186 dbg && console.log(TAG, "Parts can't be assigned...");
2187 }
2188 else if (lastMatchedPrefixSentence && !lastMatchedPostfixSentence){
2189 assignToLeftOrRight = -1;
2190 }
2191 else if (!lastMatchedPrefixSentence && lastMatchedPostfixSentence){
2192 assignToLeftOrRight = 1;
2193 }
2194 else {
2195 dbg && remainingParts.forEach(function(remainingPart,i){
2196 dbg && console.log(TAG, "RemainingPart ",i, "[",remainingPart.text+"]");
2197 });
2198
2199 // prefix ends with a linebreak or a certain sentence delimiter
2200 if (lastMatchedPrefixSentence[T_SENTENCE__TEXT].match(/(\n|[!?])\s*$/)){
2201 assignToLeftOrRight = 1;
2202 }
2203 else {
2204 assignToLeftOrRight = -1;
2205 }
2206 }
2207 if (assignToLeftOrRight>0){
2208 const texts = [];
2209 remainingParts.splice(0).reverse().forEach(function(part){
2210 texts.unshift(part.text);
2211 });
2212 texts.push(lastMatchedPostfixSentence[T_SENTENCE__TEXT]);
2213
2214 //sentencesToUpdate.push([lastMatchedPostfixSentence, texts, true]);
2215 updateTSentence(lastMatchedPostfixSentence, texts, true);
2216
2217 //lastMatchedPostfixSentence.setText(texts.join("") + lastMatchedPostfixSentence[T_SENTENCE__TEXT], false);
2218 //lastMatchedPostfixSentence[T_SENTENCE__EDIT_REV] = counter();
2219 dbg && console.log(TAG, "Append to sentence after. ",lastMatchedPostfixSentence);
2220 }
2221 else if (assignToLeftOrRight<0){
2222 const texts = [lastMatchedPrefixSentence[T_SENTENCE__TEXT]];
2223 remainingParts.splice(0).forEach(function(part){
2224 texts.push(part.text);
2225 });
2226 //sentencesToUpdate.push([lastMatchedPrefixSentence, texts, true]);
2227 updateTSentence(lastMatchedPrefixSentence, texts, true);
2228
2229 // lastMatchedPrefixSentence.setText( lastMatchedPrefixSentence[T_SENTENCE__TEXT] + texts.join(""), false);
2230 // lastMatchedPrefixSentence[T_SENTENCE__EDIT_REV] = counter();
2231 dbg && console.log("[doRefreshTargetDataFromText]Append to sentence before. ",lastMatchedPrefixSentence);
2232
2233 }
2234 // todo (later) Optionally add independent target sentences (paragraphs?) that have no corresponding source sentences
2235
2236
2237 }
2238 else if (remainingTSentences.length && remainingParts.length){
2239 console.warn(TAG, "alignment is lost. Todo!");
2240 // assign everything to the first sentence.
2241
2242 const texts = [];
2243 remainingParts.splice(0).forEach(function(part){
2244 texts.push(part.text);
2245 });
2246 const tSentence = remainingTSentences.shift();
2247
2248 //sentencesToUpdate.push([[tSentence, texts, true]]);
2249 updateTSentence(tSentence, texts, true);
2250
2251 // tSentence.setText(texts.join(""), false);
2252 // tSentence[T_SENTENCE__EDIT_REV] = counter();
2253 // todo do something fancy...
2254 }
2255 else {
2256 dbg && console.log(TAG, "doRefreshTargetDataFromText everything ok.");
2257 // everythin ok
2258 }
2259
2260 dbg && console.log(TAG, ">>>>>>>> ");
2261 }
2262
2263 return {
2264 doRefreshTargetDataFromText
2265 }
2266})
2267// = == =============== end: updateTargetSentences =============== == =
2268
2269// = == =============== updateInitialTranslation =============== == =
2270/**
2271 * When the source sentences change, request the required translations and update the target sentences.
2272 */
2273webTranslator.features.define('updateInitialTranslation', ['translationBackend', 'eventHandling', 'webTranslatorCore', 'domElements', 'stateManagement'],
2274 (translationBackend, {on, ON, notify}, webTranslatorCore, domElements, stateManagement) => {
2275
2276 const $sourceEdit = domElements.$sourceEdit;
2277 const $targetEdit = domElements.$targetEdit;
2278 /**
2279 * Update the initial translations when source sentences changed.
2280 * - check for each sentence if a new translation should be requested
2281 * - request missing translations
2282 * - the normal requests are issued with low priority (to be able to reject high frequency typing requests), but a backup request with high is always scheduled to
2283 * assure that a translation is always properly requested.
2284 *
2285 * @listens LMT_Event ON.ON__SOURCE_SENTENCES_CHANGED
2286 * @fires LMT_Event ON.ON__INITIAL_SOURCE_TRANSLATION_UPDATED
2287 * - when translations are available (even if the process ended with errors)
2288 */
2289 (function(){
2290 const dbg = false;
2291 const TAG = '[updateInitialTranslation]';
2292
2293 function doUpdateInitialTranslation(rqContext, priority){ // if priority < 0, the request is prone to be canceled by the backend in heavy load situation
2294 const state = rqContext.state
2295 const translationsToUpdate = []
2296 dbg && console.log(TAG, "doUpdateInitialTranslation", rqContext, priority);
2297 let jobs;
2298 U.scheduleNonConcurrentRequest("initialTranslation" + state.getId(), function _requestTranslation(){
2299 notify(ON.ON__REQUEST_STARTED, "initialTranslationRequest", rqContext);
2300 rqContext.onRequestEnded.push(function(){
2301 notify(ON.ON__REQUEST_ENDED, "initialTranslationRequest");
2302 });
2303 return new Promise(function(success, failure){
2304 if (!webTranslatorCore.isActiveState(state)){
2305 failure(U.scheduleNonConcurrentRequest.FAILURE_REASON__CANCELED);
2306 return;
2307 }
2308 dbg && console.log(TAG, "Collecting translation jobs...", rqContext);
2309
2310 jobs = [];
2311 (globalState[GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE]() ? state[STATE__SOURCE_SENTENCES].slice(0,1) : state[STATE__SOURCE_SENTENCES])
2312 .forEach((sourceSentence, sententeIndex) => {
2313 const translation = sourceSentence[S_SENTENCE__TRANSLATION]
2314
2315 if (translation[TRANSLATION__INITIAL_TRANSLATION_TEXT] === undefined
2316 || (
2317 translation[TRANSLATION__STATUS] === TRANSLATION_STATUS__READY__FAST_QUALITY
2318 && rqContext[REQUEST_CONTEXT__INITIAL_TRIGGER] !== TRIGGER__SOURCE_TEXT_CHANGED_WHILE_TYPING)
2319 ){
2320 const job = {
2321 'kind': JOB_KIND__DEFAULT,
2322 'raw_en_sentence': translation[TRANSLATION__SOURCE_TEXT]
2323 }
2324 if (state.isContextEnabled()){
2325 const context = doGetSourceContext(sourceSentence)
2326 job['raw_en_context_before'] = context.prefix
2327 job['raw_en_context_after'] = context.postfix
2328 }
2329 if (config[CONFIG__EXPLICITLY_REQUEST_ALTERNATIVES]){
2330 if (sententeIndex===0 && state[STATE__SOURCE_SENTENCES].length === 1){
2331 job['preferred_num_beams'] = 4
2332 }
2333 else {
2334 job['preferred_num_beams'] = 1
2335 }
2336 }
2337 jobs.push(job)
2338 translationsToUpdate.push(translation)
2339 }
2340 })
2341 if (config[CONFIG__FAST_TYPING_REQUESTS] &&
2342 rqContext[REQUEST_CONTEXT__INITIAL_TRIGGER] === TRIGGER__SOURCE_TEXT_CHANGED_WHILE_TYPING &&
2343 jobs.length === 1){
2344 jobs[0]['quality'] = 'fast';
2345 // log("[TypingRequest] sent");
2346
2347 // [dbg] Mark result as fast quality even if it is not.
2348 if (config[CONFIG__FAST_TYPING_REQUESTS__DEV_DUMMY])
2349 translationsToUpdate[0][TRANSLATION__STATUS] = TRANSLATION_STATUS__REQUESTES_FAST__DEV_TEST;
2350 }
2351 // MARKED_TYPING_REQUEST
2352 // log(TAG, "Collecting translation jobs...");
2353
2354 if (!jobs.length){ // no request necessary
2355 dbg && console.log(TAG, "No requests necessary.");
2356 notify(ON.ON__INITIAL_SOURCE_TRANSLATION_UPDATED, rqContext);
2357 success();
2358 return;
2359 }
2360 const langConfig = {
2361 'user_preferred_langs': stateManagement.getPrioritizedSourceLangs(),
2362 };
2363 if (rqContext.sourceLangHasBeenDetermined)
2364 langConfig['source_lang_computed'] = webTranslatorCore.getActiveState().getSourceLang();
2365 else
2366 langConfig['source_lang_user_selected'] = webTranslator.getSelectedSourceLang();
2367
2368 langConfig['target_lang'] = webTranslator.getSelectedTargetLang()
2369
2370 dbg && console.log(TAG, "[_requestInitialTranslations] Schedule initialTranslation");
2371 if (priority<1 && jobs.length>1){
2372 priority = 1;
2373 log(TAG, "Increase priority for large request");
2374 }
2375
2376 const batchController = translationBackend.sendTranslationJobs_batched(jobs,
2377 state.createJobSettings(langConfig, priority),
2378 conf_translationJobBatchSize);
2379 let translationToUpdateIndex = 0;
2380 notify(ON.ON__START_BATCHED_TRANSLATION_REQUEST, batchController, rqContext);
2381
2382 function handleBatch(batchController){
2383 var batchId = batchController.currentBatchId()
2384 var result = batchController.getCurrentResult();
2385 dbg && console.log(TAG, "Handeling batch", batchId)
2386
2387 if (!webTranslatorCore.isActiveState(state)) {
2388 log(TAG, "State changed -> cancel translation handling.")
2389 rqContext.onRequestEnded();
2390 success();
2391 return;
2392 }
2393 if (!result){ // empty request -> empty result
2394 log(TAG, "Empty request");
2395 notify(ON.ON__INITIAL_SOURCE_TRANSLATION_UPDATED, rqContext);
2396 success();
2397 return;
2398 }
2399
2400 let receivedSourceLang = result[TRANSLATION_RESULT__SOURCE_LANG];
2401 const receivedTargetLang = result[TRANSLATION_RESULT__TARGET_LANG];
2402 // update active language based on result
2403 if (receivedSourceLang && receivedTargetLang && batchId === 0){ // language detection is currently only performed in the first batch
2404
2405 if (rqContext.initialTrigger==TRIGGER__SOURCE_LANG_CHANGED &&
2406 webTranslator.getSelectedSourceLang() !== 'auto' &&
2407 receivedSourceLang !== webTranslator.getSelectedSourceLang()) {
2408 dbg && console.log(TAG, "IGNORING result because source lang switched by server on initialTranslation.");
2409 rqContext.onRequestEnded();
2410 success();
2411 return;
2412 }
2413 // Update ui properties
2414 if (rqContext.sourceLangHasBeenDetermined && receivedSourceLang!==state.getSourceLang()) {
2415 console.warn("BUG in Scheduler. Source language has been determined and cannot be changed. receivedSourceLang="+receivedSourceLang+" ")
2416 receivedSourceLang = state.getSourceLang(); // Workarround for now. Should be fixed in the Scheduler.
2417 }
2418 if (result["source_lang_is_confident"]) {
2419 dbg && console.log(TAG, "pushPreferredLang("+receivedSourceLang+")");
2420 webTranslator.pushPreferredLang(receivedSourceLang)
2421 }
2422
2423 if ( state.getSourceLang()!==receivedSourceLang || state.getTargetLang()!==receivedTargetLang){
2424 log(TAG, "Switching lang codes to ", receivedSourceLang, receivedTargetLang);
2425 rqContext.onRequestEnded();
2426
2427 if (state.getTargetLang() && state.getTargetLang()!==receivedTargetLang){
2428 console.warn("Should not happen: state.getTargetLang()!==receivedTargetLang ",state.getTargetLang(), receivedTargetLang)
2429 }
2430 // if the language is changed based only on the translation of a small part of a longer text
2431 // --> use splitSentence request (which uses the whole text to detect the source language) and try again.
2432 if (!globalState[GSTATE__FORCE_SENTENCE_SPLITTING_REQUESTS_FOR_STABILITY]() &&
2433 !rqContext.splitSentenceRequestUsed &&
2434 state[STATE__SOURCE_SENTENCES].length>1 ){
2435 globalState[GSTATE__FORCE_SENTENCE_SPLITTING_REQUESTS_FOR_STABILITY](true);
2436 setTimeout(() => notify(ON.ON__SOURCE_TEXT_CHANGED,
2437 new RequestContext(webTranslatorCore.getActiveState(), rqContext.initialTrigger)))
2438 }
2439 else{
2440 stateManagement.changedSourceLangDetected(receivedSourceLang)
2441 }
2442 success();
2443 return;
2444 }
2445 }
2446 const translationResults = result[TRANSLATION_RESULT__TRANSLATIONS];
2447
2448 //console.log(translationResults);
2449
2450 let emptyBeams = false;
2451 translationResults.forEach(function(translationResult){
2452 var translation = translationsToUpdate[translationToUpdateIndex];
2453
2454 // [dbg] Mark result as fast quality even if it is not.
2455 if (translation[TRANSLATION__STATUS] === TRANSLATION_STATUS__REQUESTES_FAST__DEV_TEST)
2456 translationResult['quality'] = 'fast';
2457
2458 ++translationToUpdateIndex;
2459 if (translationResult['beams'].length){
2460 var translationTexts = [];
2461 translationResult['beams'].forEach(function(beam){
2462 var text = beam[RECORD__POSTPROCESSED_SENTENCE];
2463 if (!translationTexts.some(function(t){ return t===text})) // prevent duplicates
2464 translationTexts.push(text);
2465 });
2466 translation[TRANSLATION__INITIAL_TRANSLATION_TEXT] = translationTexts.shift();
2467 if (translationResult['quality']==='fast'){
2468 translation[TRANSLATION__STATUS] = TRANSLATION_STATUS__READY__FAST_QUALITY;
2469 } else {
2470 translation[TRANSLATION__STATUS] = TRANSLATION_STATUS__READY;
2471 }
2472
2473 // [dbg] Simulate fast request by setting the text to upper case
2474 if (config[CONFIG__FAST_TYPING_REQUESTS__DEV_DUMMY] &&
2475 translation[TRANSLATION__STATUS] === TRANSLATION_STATUS__READY__FAST_QUALITY){
2476 translation[TRANSLATION__INITIAL_TRANSLATION_TEXT] = translation[TRANSLATION__INITIAL_TRANSLATION_TEXT].toUpperCase();
2477 }
2478 translation[TRANSLATION__INITIAL_TRANSLATION_RESULT] = translationResult;
2479 translation[TRANSLATION__ALTERNATIVE_INITIAL_TRANSLATION_TEXTS] = translationTexts;
2480 }else{
2481 translation[TRANSLATION__INITIAL_TRANSLATION_TEXT] = logging ? "[SENTENCE COULD NOT BE TRANSLATED]" : "";
2482 emptyBeams = true;
2483 }
2484 });
2485 if (emptyBeams){
2486 console.warn(TAG, "Empty beams!");
2487 console.warn(TAG, "Jobs:", jobs);
2488 console.warn(TAG, "Result:", result);
2489 }
2490 if (translationResults.length<batchController.getNumberOfJobsInCurrentBatch()){
2491 log(TAG, "Finished with errors: Partial result!");
2492 batchController.stopped(true);
2493 notify(ON.ON__PARTIAL_TRANSLATION_RECEIVED); // TODO !!!!!!!! reactivate
2494 success();
2495 } else if (translationResults.length!=batchController.getNumberOfJobsInCurrentBatch()){
2496 log(TAG, "Finished with errors: Invalid number of results", translationResults.length, batchController.getNumberOfJobsInCurrentBatch());
2497 batchController.stopped(true);
2498 notify(ON.ON__REQUEST_FAILED, "translation", createLMTError('LMT_ERROR_TYPE__UNEXPECTED'));
2499 success();
2500 } else if (batchController.finished()){
2501 success();
2502 } else {
2503 dbg && console.log(TAG, "Next...");
2504 batchController.next().then(handleBatch, handleError);
2505 }
2506 }
2507 function handleError(lmtError){
2508 rqContext.onRequestEnded();
2509 batchController.stopped(true);
2510 notify(ON.ON__REQUEST_FAILED, "translation", lmtError);
2511 if (priority<0){
2512 failure(lmtError);
2513 } else {
2514 dbg && console.log(TAG, "Finished with errors");
2515 success();
2516 }
2517 }
2518 batchController.next().then(handleBatch, handleError);
2519 });
2520 }, 10000).then(function(_result){
2521 dbg && console.log(TAG, "Finished");
2522 notify(ON.ON__INITIAL_SOURCE_TRANSLATION_UPDATED, rqContext);
2523
2524 }, function(canceled_or_lmtError){
2525 if (canceled_or_lmtError===U.scheduleNonConcurrentRequest.FAILURE_REASON__CANCELED){
2526 log(TAG, "CANCELED (ignore...)");
2527 } else if (priority<0){
2528 console.warn(canceled_or_lmtError);
2529 log(TAG, "Low-prio request failed.");
2530 globalState[GSTATE__TYPING_REQUESTS_BLOCKED_UNTIL] = Date.now() + conf_blockTypingRequestsTime;
2531
2532 notify(ON.ON__REQUEST_STARTED, "backupTranslateRequest"); // Keep animation running
2533 setTimeout(function(){
2534 notify(ON.ON__REQUEST_ENDED, "backupTranslateRequest");
2535 }, conf_sendBackupRequestTime+50);
2536
2537 U.setTimeout_consolidated('assureInitialTranslationIsRequested', function(){
2538 log(TAG, "Send backup request")
2539 notify(ON.ON__SOURCE_TEXT_CHANGED, new RequestContext(webTranslatorCore.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED));
2540 }, conf_sendBackupRequestTime);
2541 } else { // should not happen
2542 console.error(canceled_or_lmtError);
2543 }
2544 rqContext.onRequestEnded();
2545
2546 });
2547 }
2548 //var resetWaitingIndicatorCounter = U.createCallOnce()
2549 on(ON.ON__SOURCE_SENTENCES_CHANGED, function _requestInitialTranslations(rqContext){
2550 const state = rqContext.state;
2551 dbg && console.log(TAG, "ON.ON__SOURCE_SENTENCES_CHANGED", state.getSourceLang(), state.getTargetLang(), rqContext.initialTrigger);
2552 (rqContext[REQUEST_CONTEXT__ON_SOURCE_SENTENCES_UPDATED])();
2553
2554 doUpdateInitialTranslation(rqContext, rqContext.initialTrigger===TRIGGER__SOURCE_TEXT_CHANGED_WHILE_TYPING ? conf_typingRequestPriority : 1.0);
2555 });
2556 })();
2557
2558 /** ? [ON.ON__INITIAL_SOURCE_TRANSLATION_UPDATED]
2559 - reset the state[STATE__TARGET_TEXT]
2560 - reset the state[STATE__TARGET_SENTENCES]
2561 - reset the target textarea's text
2562
2563 Todo: (claudius) first update/create targetSentences, then update the text based on the sentences (not the other way around)
2564 */
2565 on(ON.ON__INITIAL_SOURCE_TRANSLATION_UPDATED, function _updateTargetEditWithInitialTranslations(rqContext){
2566 const dbg = false;
2567
2568 dbg && console.log("[updateTarget] ON.ON__INITIAL_SOURCE_TRANSLATION_UPDATED");
2569 var state = rqContext.state;
2570 rqContext.onRequestEnded(); // no further processing
2571
2572 // <<<<<<<<<<<<<<<<<<<< #4093
2573 // Todo: Claudius What happens if a source sentence updated its whitespaces?
2574 // Todo: Handle sentences with pending translations
2575 // Todo: update target text
2576 if (config[CONFIG__UPDATE_TARGET_SENTENCES]){
2577 var remainingSourceSentences = state[STATE__SOURCE_SENTENCES].slice(0);
2578 var remainingTargetSentences = state[STATE__TARGET_SENTENCES].splice(0);
2579 var targetSentences = state[STATE__TARGET_SENTENCES];
2580
2581
2582 var updateTargetSentenceText = function _updateTargetSentenceText(targetSentence){
2583 var sourceSentence = targetSentence.getSourceSentence();
2584 var translation = sourceSentence[S_SENTENCE__TRANSLATION];
2585
2586 if (sourceSentence[S_SENTENCE__EDIT_REV]>=targetSentence[T_SENTENCE__EDIT_REV]){
2587 var s = [];
2588 if (sourceSentence[S_SENTENCE__WHITESPACES_BEFORE_REV]>targetSentence[T_SENTENCE__WHITESPACES_BEFORE_REV]){
2589 s.push(sourceSentence[S_SENTENCE__WHITESPACES_BEFORE]);
2590 targetSentence[T_SENTENCE__WHITESPACES_BEFORE_REV] = sourceSentence[S_SENTENCE__WHITESPACES_BEFORE_REV];
2591 }else{
2592 s.push(targetSentence.getWhitespacesBefore());
2593 }
2594
2595 var t = translation[TRANSLATION__INITIAL_TRANSLATION_TEXT];
2596 if (t===undefined){ // no current translation -> show latest known translation
2597 targetSentence[T_SENTENCE__EDIT_REV] = -1; // text is not final
2598 var prevTs = sourceSentence[S_SENTENCE__PREVIOUS_TRANSLATIONS] ? sourceSentence[S_SENTENCE__PREVIOUS_TRANSLATIONS].slice(0) : [];
2599 for (;;){
2600 var prevT = prevTs.shift();
2601 if (!prevT){ // no translation received -> show "...""
2602 if (!globalState[GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE]() && (!s.length || s[s.length-1]!=="[...] "))
2603 s.push("[...] ");
2604 break;
2605 }else if (prevT[TRANSLATION__INITIAL_TRANSLATION_TEXT]){ // latest previous translation
2606 s.push(prevT[TRANSLATION__INITIAL_TRANSLATION_TEXT]);
2607 break;
2608 }
2609 }
2610 }else{
2611 targetSentence[T_SENTENCE__EDIT_REV] = sourceSentence[S_SENTENCE__EDIT_REV]; // same version as source
2612 s.push(t);
2613 }
2614 if (sourceSentence[S_SENTENCE__WHITESPACES_AFTER_REV]>targetSentence[T_SENTENCE__WHITESPACES_AFTER_REV]){
2615 s.push(sourceSentence[S_SENTENCE__WHITESPACES_AFTER]);
2616 targetSentence[T_SENTENCE__WHITESPACES_AFTER_REV] = sourceSentence[S_SENTENCE__WHITESPACES_AFTER_REV];
2617 }else{
2618 s.push(targetSentence.getWhitespacesAfter());
2619 }
2620 targetSentence[T_SENTENCE__EDIT_SNAPSHOTS].splice(0); // clear former edit snapshots (no longer relevant for new text)
2621 targetSentence.setText(s.join(""), t!==undefined);
2622 }else{
2623 if (sourceSentence[S_SENTENCE__WHITESPACES_BEFORE_REV]>targetSentence[T_SENTENCE__WHITESPACES_BEFORE_REV]){
2624 targetSentence[T_SENTENCE__WHITESPACES_BEFORE_REV] = sourceSentence[S_SENTENCE__WHITESPACES_BEFORE_REV];
2625 targetSentence[T_SENTENCE__TEXT] = sourceSentence[S_SENTENCE__WHITESPACES_BEFORE] + targetSentence[T_SENTENCE__TEXT].trimStart();
2626 targetSentence[T_SENTENCE__PARTS] = undefined;
2627 dbg && console.log("[updateTarget] update whitespaces before ", "[" + sourceSentence[S_SENTENCE__WHITESPACES_BEFORE] + "]");
2628 }
2629 if (sourceSentence[S_SENTENCE__WHITESPACES_AFTER_REV]>targetSentence[T_SENTENCE__WHITESPACES_AFTER_REV]){
2630 targetSentence[T_SENTENCE__WHITESPACES_AFTER_REV] = sourceSentence[S_SENTENCE__WHITESPACES_AFTER_REV];
2631 targetSentence[T_SENTENCE__TEXT] = targetSentence[T_SENTENCE__TEXT].trimEnd() + sourceSentence[S_SENTENCE__WHITESPACES_AFTER];
2632 targetSentence[T_SENTENCE__PARTS] = undefined;
2633 dbg && console.log("[updateTarget] update whitespaces after ", "[" + sourceSentence[S_SENTENCE__WHITESPACES_AFTER] + "]");
2634 }
2635
2636
2637 dbg && console.log("[updateTarget] target preserved ", "[" + targetSentence[T_SENTENCE__TEXT] + "]");
2638 }
2639 }
2640
2641 // consume unchanged sentences prefix
2642 while (remainingTargetSentences.length && remainingTargetSentences[0].getSourceSentence()===remainingSourceSentences[0]){ // todo handle updated whitespaces
2643 dbg && console.log("[updateTarget] consume front");
2644 var targetSentence = remainingTargetSentences.shift();
2645 updateTargetSentenceText(targetSentence);
2646 targetSentences.push(targetSentence);
2647 remainingSourceSentences.shift();
2648 }
2649
2650 // consume unchanged sentences postfix
2651 const targetSentencesPostfix = [];
2652 while (remainingTargetSentences.length &&
2653 remainingTargetSentences[remainingTargetSentences.length-1].getSourceSentence()===remainingSourceSentences[remainingSourceSentences.length-1]){ // todo handle updated whitespaces
2654 dbg && console.log("[updateTarget] consume back");
2655 const targetSentence = remainingTargetSentences.pop();
2656 updateTargetSentenceText(targetSentence);
2657 targetSentencesPostfix.unshift(targetSentence);
2658 remainingSourceSentences.pop();
2659 }
2660
2661 // re-assign changed sentences (todo: if the text overlapps a little)
2662 while (remainingSourceSentences.length && remainingTargetSentences.length){ // todo handle updated whitespaces
2663 dbg && console.log("[updateTarget] assign");
2664
2665 const sourceSentence = remainingSourceSentences.shift();
2666 const targetSentence = remainingTargetSentences.shift();
2667 targetSentence.setSourceSentence(sourceSentence);
2668 updateTargetSentenceText(targetSentence);
2669 targetSentences.push(targetSentence);
2670 }
2671 // add new new sentences
2672 // ....
2673 while (remainingSourceSentences.length){
2674 dbg && console.log("[updateTarget] add new");
2675 const sourceSentence = remainingSourceSentences.shift();
2676 const targetSentence = createTargetSentence("");
2677 targetSentence.setSourceSentence(sourceSentence);
2678 updateTargetSentenceText(targetSentence);
2679
2680 targetSentences.push(targetSentence);
2681 }
2682 while (remainingTargetSentences.length){
2683 remainingTargetSentences.shift();
2684 dbg && console.log("[updateTarget] remove");
2685 }
2686
2687 while (targetSentencesPostfix.length){
2688 const targetSentence = targetSentencesPostfix.shift();
2689 targetSentences.push(targetSentence);
2690 }
2691 // update text
2692 const targetStrings = [];
2693 targetSentences.forEach(function(tSentence){targetStrings.push(tSentence[T_SENTENCE__TEXT]);});
2694 state[STATE__TARGET_TEXT] = targetStrings.join("");
2695
2696 }else{ // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2697 const s = [];
2698 (globalState[GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE]() ? state[STATE__SOURCE_SENTENCES].slice(0,1) : state[STATE__SOURCE_SENTENCES])
2699 .forEach(function(sourceSentence){
2700 s.push(sourceSentence[S_SENTENCE__WHITESPACES_BEFORE]);
2701 var translationsObject = sourceSentence[S_SENTENCE__TRANSLATION];
2702 var t = translationsObject[TRANSLATION__INITIAL_TRANSLATION_TEXT];
2703 if (t===undefined){ // no current translation -> show latest known translation
2704 var prevTs = sourceSentence[S_SENTENCE__PREVIOUS_TRANSLATIONS] ? sourceSentence[S_SENTENCE__PREVIOUS_TRANSLATIONS].slice(0) : [];
2705 for (;;){
2706 var prevT = prevTs.shift();
2707 if (!prevT){ // no translation received -> show "...""
2708 if (!s.length || s[s.length-1]!=="... ")
2709 s.push("... ");
2710 break;
2711 }else if (prevT[TRANSLATION__INITIAL_TRANSLATION_TEXT]){ // latest previous translation
2712 s.push(prevT[TRANSLATION__INITIAL_TRANSLATION_TEXT]);
2713 break;
2714 }
2715 }
2716 }else{
2717 s.push(t);
2718 /*
2719 // show alternatives
2720 if (i==0 && state[STATE__SOURCE_SENTENCES].length==1){
2721 translationsObject[TRANSLATION__ALTERNATIVE_INITIAL_TRANSLATION_TEXTS].forEach(function(t){
2722 s.push("\n"+t);
2723 });
2724 }*/
2725 }
2726 s.push(sourceSentence[S_SENTENCE__WHITESPACES_AFTER]);
2727 });
2728 const targetText = s.join("");
2729 state[STATE__TARGET_TEXT] = targetText;
2730 const targetSentences = [];
2731 state[STATE__SOURCE_SENTENCES].forEach(function(sourceSentence){
2732 const sentenceTargetText = sourceSentence[S_SENTENCE__WHITESPACES_BEFORE] +
2733 (sourceSentence[S_SENTENCE__TRANSLATION][TRANSLATION__INITIAL_TRANSLATION_TEXT] || "") +
2734 sourceSentence[S_SENTENCE__WHITESPACES_AFTER];
2735 const targetSentence = createTargetSentence(sentenceTargetText);
2736 targetSentence.setSourceSentence(sourceSentence);
2737 targetSentences.push(targetSentence);
2738 });
2739 state[STATE__TARGET_SENTENCES] = targetSentences;
2740 }
2741
2742
2743 // Block for additional initial translations.
2744 if (webTranslatorCore.isActiveState(state))
2745 {
2746 const dbg = false;
2747 dbg && console.log("[MobileResult] translation received", $translationsAsText);
2748 const state = rqContext.state;
2749 //rqContext.onRequestEnded(); // no further processing
2750
2751 // window.matchMedia && window.matchMedia('only screen') && (function(){
2752 const addCopyButton = function _addCopyButton($node, text){
2753 { // if (!DeviceProps.isIPad && !DeviceProps.isIPhone){
2754 $("<div></div>").css({ // placeholder to prevent the copy button overlapping the text.
2755 "display": "inline-block",
2756 "width":"25px",
2757 "height":"10px"
2758 //"outline":"1px solid red"
2759 }).appendTo($node);
2760
2761 $("<button class='lmt__translations_as_text__copy_button'></button>")
2762 .click(function(){
2763 const $button = $(this);
2764 $button.addClass('lmt__translations_as_text__copy_button--active');
2765 setTimeout(function(){
2766 $button.removeClass('lmt__translations_as_text__copy_button--active');
2767 },100);
2768 const textToCopy = U.value(text);
2769 notify(ON.ON__INTERCEPT_COPIED_TRANSLATION, textToCopy);
2770 LMT_UI_Utils.copyToClipboard(textToCopy(), $node).then( ()=>{
2771 LMT_UI_FeedbackMessage.show({
2772 target: $button,
2773 content: getLocalizedText('translator/copyText.feedback', "Text has been copied"),
2774 timeout: 1000
2775 });
2776
2777 });
2778 }).appendTo($node);
2779 }
2780 }
2781 const createTranslationEntry = function _addTranslationEntry(text, _addCopyButton, isMain){
2782 text = text.trim();
2783 var doAddCopyButton = _addCopyButton && text.length>0;
2784 var $inner = $("<button class='lmt__translations_as_text__text_btn'></button>").text(text)
2785 .click((_e) => {
2786 _updateTextareaText($targetEdit, text);
2787 });
2788 var $entry = $("<p class='lmt__translations_as_text__item' dl-test='translator-target-result-as-text-entry'></p>").append($inner);
2789 if (isMain) $entry.addClass("lmt__translations_as_text__main_translation");
2790 if (doAddCopyButton)
2791 addCopyButton($entry, text);
2792 return $entry;
2793 }
2794
2795
2796 $translationsAsText
2797 .css('font', $sourceEdit.css('font'))
2798 .css('font-size', $sourceEdit.css('font-size'));
2799
2800 const $newTranslationsAsText = $('<div></div>');
2801
2802 var hasUnfinishedResults = false;
2803 var s = [];
2804 (globalState[GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE]() ? state[STATE__SOURCE_SENTENCES].slice(0,1) : state[STATE__SOURCE_SENTENCES])
2805 .forEach(function(sourceSentence, i){
2806 s.push(sourceSentence[S_SENTENCE__WHITESPACES_BEFORE]);
2807 var targetSentence = state[STATE__TARGET_SENTENCES][i];
2808 var t = targetSentence[T_SENTENCE__TEXT];
2809 if (!t){
2810 var translationsObject = sourceSentence[S_SENTENCE__TRANSLATION];
2811 t = translationsObject[TRANSLATION__INITIAL_TRANSLATION_TEXT];
2812 }
2813
2814 if (t===undefined){ // no current translation -> show latest known translation
2815 hasUnfinishedResults = true;
2816 var prevTs = sourceSentence[S_SENTENCE__PREVIOUS_TRANSLATIONS] ? sourceSentence[S_SENTENCE__PREVIOUS_TRANSLATIONS].slice(0) : [];
2817 for (;;){
2818 var prevT = prevTs.shift();
2819 if (!prevT){ // no translation received -> show "...""
2820 if (!s.length || s[s.length-1]!=="... ")
2821 s.push("... ");
2822 break;
2823 }else if (prevT[TRANSLATION__INITIAL_TRANSLATION_TEXT]){ // latest previous translation
2824 s.push(prevT[TRANSLATION__INITIAL_TRANSLATION_TEXT]);
2825 break;
2826 }
2827 }
2828 }else{
2829 s.push(t.trim());
2830
2831 }
2832 s.push(sourceSentence[S_SENTENCE__WHITESPACES_AFTER]);
2833 });
2834
2835 var targetText = s.join("").trim();
2836 $newTranslationsAsText.append(createTranslationEntry(targetText, !hasUnfinishedResults, true));
2837 var lowerT = targetText.toLowerCase().replace(new RegExp("[!?\\.,]", 'g'), "");
2838 var seenTranslations = [lowerT];
2839 let numAdditional = 0;
2840 var maxLength = ((window.width>800)?45:30);
2841 if (state[STATE__SOURCE_SENTENCES].length==1 && !hasUnfinishedResults && !rqContext[REQUEST_CONTEXT__TARGET_IS_PREDEFINED]){
2842 var sourceSentence = state[STATE__SOURCE_SENTENCES][0];
2843 let sentenceMinLength = 0;
2844 if (state.getSourceLang()!="EN" && state.getTargetLang()!="EN") {
2845 sentenceMinLength = 5;
2846 }
2847 var sentenceLength = sourceSentence[S_SENTENCE__RAW_TRIMMED_TEXT].length;
2848 dbg && console.log("[updateTarget] Sentence length:", sentenceLength);
2849 if (sentenceLength<=60 && sentenceLength>=sentenceMinLength && sourceSentence[S_SENTENCE__TRANSLATION][TRANSLATION__ALTERNATIVE_INITIAL_TRANSLATION_TEXTS]){
2850 var translationsObject = sourceSentence[S_SENTENCE__TRANSLATION];
2851 translationsObject[TRANSLATION__ALTERNATIVE_INITIAL_TRANSLATION_TEXTS].forEach(function(t){
2852 if (numAdditional==3 && !DeviceProps.isMobilePhone) return;
2853 t = t.trim();
2854 dbg && console.log("[updateTarget] Alternative Translation length: ", t.length);
2855 if (t.length>maxLength) return;
2856 lowerT = t.toLowerCase().toLowerCase().replace(new RegExp("[!?\\.,]", 'g'), "");
2857 if (t.length ){
2858 var parts = lowerT.split(" ");
2859 var skip = seenTranslations.indexOf(lowerT)!==-1;
2860 if (!skip) {
2861 parts.forEach(function(part1, i){
2862 if (part1.length>2)
2863 parts.forEach(function(part2, j){
2864 if (part2.startsWith(part1) && i!=j)
2865 skip = true;
2866 });
2867 });
2868 }
2869 if (!skip){
2870 $newTranslationsAsText.append(createTranslationEntry(t, true, false));
2871 numAdditional++;
2872 //addCopyButton($("<p></p>").text(t + spacerText).appendTo($newTranslationsAsText), t);
2873 //$("<textarea disabled='true'></textarea>").val(t);
2874 seenTranslations.push(lowerT);
2875 }else{
2876 // log("Skip", t);
2877 }
2878 }
2879 });
2880 }
2881 }
2882 // log("seenTranslations:", seenTranslations);
2883
2884 if (numAdditional>0) {
2885 $("<h2 class='lmt__translations_as_text__header'></h2>")
2886 .html(getLocalizedText('translator/alternativeTranslations', "Alternatives:"))
2887 .prependTo($newTranslationsAsText);
2888 if (queryVars["without_alternatives"]===undefined) {
2889 $rootContainer.addClass("lmt--showing_alternatives");
2890 $rootContainer.find('.lmt__sides_container').css('overflow','hidden'); // set overflow hidden during the opening animation to prevent overlapping.
2891 setTimeout(function(){
2892 $rootContainer.find('.lmt__sides_container').css('overflow',''); // clear overflow after animation to prevent problems with popups
2893 }, 400);
2894 }
2895 } else {
2896 $rootContainer.removeClass("lmt--showing_alternatives");
2897 }
2898
2899 if ($newTranslationsAsText.html() !== $translationsAsText.html()){
2900 $translationsAsText.empty().append($newTranslationsAsText.children());
2901 }
2902 dbg && console.log("[updateTarget] Alternative initial translations:", numAdditional);
2903 }
2904
2905 if (webTranslatorCore.isActiveState(state)) {
2906 $targetEdit.val(state[STATE__TARGET_TEXT]);
2907 checkForChangedTargetText('update');
2908
2909 // Inform external components about completed translation
2910 notify(ON.ON__TRANSLATION_READY, {
2911 type: "initialTranslation",
2912 // pendingRequests: // todo!
2913 });
2914
2915 }
2916 webTranslator.features.get('smartTargetEditing').closePopup(); // todo: this is the wrong position for that
2917
2918 U.notify('LMT/translationReady', $sourceEdit.val(), $targetEdit.val(), state.getSourceLang(), state.getTargetLang());
2919 });
2920
2921 return {
2922 };
2923
2924});
2925// = == =============== end: updateInitialTranslation =============== == =
2926
2927
2928
2929
2930
2931
2932
2933// = == =============================== View helper functions ================================ == =
2934
2935
2936function getTargetEditCursorOffset(){
2937 return $targetEdit.prop("selectionStart");
2938}
2939function getSourceEditCursorOffset(){
2940 return $sourceEdit.prop("selectionStart");
2941}
2942
2943/**
2944 * Returns the TargetSentence at a given cursor offset
2945 * @param {LMT_State} state
2946 * @param {int} cursorOffset
2947 * @return {object} {sentence: LMT_TargetSentence, sentenceCursorOffset: int, cursorOffset: int}
2948 * */
2949function getSentencePosForTargetCursorOffset(state, cursorOffset){
2950 let relCursorOffset = cursorOffset;
2951
2952 const sentences = state[STATE__TARGET_SENTENCES];
2953 for (let sentenceIndex=0; sentenceIndex<sentences.length; ++sentenceIndex){
2954 const sentence = sentences[sentenceIndex];
2955 const sentenceText = sentence.getText();
2956 const sentenceTextLength = sentenceText.length;
2957
2958 if (relCursorOffset < sentenceTextLength || // inside sentence
2959 (relCursorOffset === sentenceTextLength && ( // directly behind the sentence...
2960 sentenceTextLength === 0 || // empty sentence
2961 sentenceIndex === sentences.length-1 || // last sentence
2962 !sentenceText.endsWith("\n")) // not the last sentence in the line
2963 )) {
2964 return {
2965 sentence: sentence,
2966 sentenceCursorOffset: relCursorOffset,
2967 cursorOffset,
2968 sentenceIndex
2969 };
2970 }
2971 relCursorOffset -= sentenceTextLength;
2972 }
2973 return null;
2974}
2975
2976function getTargetEditPixelOffsetForCursorOffset(cursorPos){
2977 return LMT_UI_Utils.getTextareaPixelOffsetForCursorOffset($targetEdit, cursorPos);
2978}
2979
2980/**
2981 * Returns the SourceSentence at a given cursor offset
2982 * @param {LMT_State} state
2983 * @param {int} cursorOffset
2984 * @return {object} {sentence: LMT_SourceSentence, sentenceCursorOffset: int, cursorOffset: int}
2985 * */
2986function getSentencePosForSourceCursorOffset(state, cursorOffset){
2987 let relCursorOffset = cursorOffset;
2988
2989 const sentences = state[STATE__SOURCE_SENTENCES];
2990 for (let sentenceIndex=0; sentenceIndex<sentences.length; ++sentenceIndex){
2991 const sentence = sentences[sentenceIndex];
2992 const sentenceText = sentence.getText();
2993 const sentenceTextLength = sentenceText.length;
2994
2995 if (relCursorOffset < sentenceTextLength || // inside sentence
2996 (relCursorOffset === sentenceTextLength && ( // directly behind the sentence...
2997 sentenceTextLength === 0 || // empty sentence
2998 sentenceIndex === sentences.length-1 || // last sentence
2999 !sentenceText.endsWith("\n")) // not the last sentence in the line
3000 )) {
3001 return {
3002 sentence: sentence,
3003 sentenceCursorOffset: relCursorOffset,
3004 cursorOffset,
3005 sentenceIndex
3006 };
3007 }
3008 relCursorOffset -= sentenceTextLength;
3009 }
3010 return null;
3011}
3012
3013
3014
3015// global listeners
3016(function(){
3017 $(document).click(function(inEvent) {
3018 notify(ON.ON__ANYCLICK, $(inEvent.target), inEvent);
3019 });
3020 let lastActiveElement;
3021 function checkActiveElement(_evt){
3022 if (document.activeElement!==lastActiveElement){
3023 lastActiveElement = document.activeElement;
3024 notify(ON.ON__ACTIVE_ELEMENT_CHANGED, $(lastActiveElement));
3025 }
3026 }
3027 window.addEventListener('focus', checkActiveElement, true);
3028})();
3029
3030
3031function getLocalizedText(key, _defaultValue){
3032 return dlTexts.get(key, _defaultValue);
3033}
3034webTranslator.features.register('localizedTexts', {
3035 get: getLocalizedText
3036});
3037
3038function preventPropagationForEvent(inEvent) {
3039 inEvent.stopPropagation();
3040 inEvent.preventDefault();
3041 inEvent.returnValue = false;
3042 return false;
3043}
3044
3045// = == =============================== end: View helper functions ================================ == =
3046
3047
3048// = == =============================== Event handlers ================================ == =
3049
3050const REQUEST_CONTEXT__STATE = 'state';
3051const REQUEST_CONTEXT__INITIAL_TRIGGER = 'initialTrigger';
3052const REQUEST_CONTEXT__SPLIT_SENTENCE_REQUEST_USED = 'splitSentenceRequestUsed';
3053const REQUEST_CONTEXT__ON_REQUEST_ENDED = 'onRequestEnded';
3054const REQUEST_CONTEXT__ON_SOURCE_SENTENCES_UPDATED = 'onSentenceSplittingReceived';
3055const REQUEST_CONTEXT__TARGET_IS_PREDEFINED = 'REQUEST_CONTEXT__TARGET_IS_PREDEFINED'
3056
3057
3058function RequestContext(state, initialTrigger){
3059 this[REQUEST_CONTEXT__STATE] = state;
3060 this[REQUEST_CONTEXT__INITIAL_TRIGGER] = initialTrigger;
3061 this[REQUEST_CONTEXT__ON_SOURCE_SENTENCES_UPDATED] = U.createCallOnce();
3062 this[REQUEST_CONTEXT__SPLIT_SENTENCE_REQUEST_USED] = undefined;
3063 this[REQUEST_CONTEXT__ON_REQUEST_ENDED] = U.createCallOnce();
3064 this.sourceLangHasBeenDetermined = undefined;
3065}
3066
3067
3068let checkForChangedSourceEdit; // to be used by other features
3069function resetSourceText(rqContext, newText){
3070 _updateTextareaText($sourceEdit, newText);
3071 notify(ON.ON__SOURCE_TEXT_CHANGED, rqContext);
3072}
3073
3074/**
3075 * Listen for changes of the source text
3076 * @fires LMT_Event ---> ON.ON__SOURCE_TEXT_CHANGED (requestContext)
3077*/
3078(function(){
3079 let prevText = "";
3080 function checkForChangedSourceText(_applyHack, rqContext){
3081 var sourceText = $sourceEdit.val();
3082 if (prevText===sourceText){
3083 return false;
3084 } else {
3085 prevText = sourceText;
3086 //log("notify ON.ON__SOURCE_TEXT_CHANGED because of "+e.type)
3087 notify(ON.ON__SOURCE_TEXT_CHANGED, rqContext);
3088 if (_applyHack){
3089 // Hack, damit auf einem iPhone nach dem Einfügen (paste) der Cursor in den sichtbaren Bereich gescrollt ist.
3090 setTimeout(function() {
3091 if ($sourceEdit.is(":focus")) { $sourceEdit.blur().focus(); log("blur and focus() on source"); }
3092 if ($targetEdit.is(":focus")) { $targetEdit.blur().focus(); log("blur and focus() on target"); }
3093 });
3094 }
3095 return true;
3096 }
3097 }
3098 function hasFastTranslations(state){
3099 return state[STATE__SOURCE_SENTENCES].some(function(sSentence){
3100 return sSentence[S_SENTENCE__TRANSLATION] &&
3101 sSentence[S_SENTENCE__TRANSLATION][TRANSLATION__STATUS] === TRANSLATION_STATUS__READY__FAST_QUALITY;
3102 });
3103 }
3104 checkForChangedSourceEdit = function(_rqContext){
3105 checkForChangedSourceText(false, _rqContext || new RequestContext(webTranslator.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED));
3106 };
3107 $sourceEdit.on('change keyup input', function _checkIfSourceTextHasChanged(e) { // input
3108 // ignore insertText input events to ignore programmatically set text
3109 // (interferes with the share feature with edited target text)
3110 if (e.originalEvent.type==='input' && e.originalEvent.inputType === 'insertText')
3111 return; // ignore all input
3112 if (Date.now()>globalState[GSTATE__TYPING_REQUESTS_BLOCKED_UNTIL]){
3113 log("[_checkIfSourceTextHasChanged] change|keyup: ", e.type);
3114 checkForChangedSourceText(false, new RequestContext(webTranslator.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED_WHILE_TYPING) );
3115 } else {
3116 // todo: don't show animation if only navigation keys are pressed
3117 notify(ON.ON__REQUEST_STARTED, "backupTranslateRequest"); // show animation even before starting the request
3118 setTimeout(function(){
3119 notify(ON.ON__REQUEST_ENDED, "backupTranslateRequest");
3120 }, conf_sendBackupRequestTime+50);
3121 }
3122 U.setTimeout_consolidated('assureInitialTranslationIsRequested', function(){
3123 log("[_checkIfSourceTextHasChanged] BACKUP REQUEST", hasFastTranslations(webTranslator.getActiveState()));
3124 checkForChangedSourceText(false, new RequestContext(webTranslator.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED)) ||
3125 (hasFastTranslations(webTranslator.getActiveState()) &&
3126 notify(ON.ON__SOURCE_SENTENCES_MAY_REQUIRE_UPDATE, new RequestContext(webTranslator.getActiveState(), TRIGGER__FAILSAFE_CHECK)));
3127 }, conf_sendBackupRequestTime);
3128 });
3129 // delayed check (changed text might not be available yet)
3130 $sourceEdit.on('paste drop', function _checkIfSourceTextHasChanged(e) {
3131 setTimeout(function(){
3132 checkForChangedSourceText(DeviceProps.is_touch_device && e.type=="paste", new RequestContext(webTranslator.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED));
3133 });
3134 });
3135 // check for text which was inserted before this script had been initialized
3136 setTimeout(function(){
3137 checkForChangedSourceText(false, new RequestContext(webTranslator.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED));
3138 }, 250);
3139 // count how many chars have been typed
3140 let typedChars = 0;
3141 $sourceEdit.on("keyup", function _countTypedChars() {
3142 typedChars+=1;
3143 U.setTimeout_consolidated("typed_chars", function() {
3144 dlStats.countStats("c", function(x) { return x+typedChars; }, 0);
3145 typedChars=0;
3146 },3000);
3147 });
3148})();
3149
3150
3151/** Listen on changes of the target text
3152 --> [ON.ON__TARGET_EDIT_TEXT_CHANGED] (reason)
3153 Note: The reason parameter can not yet reliably detect programmatic changes to the target edit. This should be fixed if needed.
3154 Reasons:
3155 initial
3156 input
3157 update
3158*/
3159let checkForChangedTargetText; // to be used by other features
3160{
3161 let prevText = "";
3162 const _checkForChangedTargetText = function(reason, _evt){
3163 const text = $targetEdit.val();
3164 if (prevText!==text){
3165 log("TARGET CHANGED BECAUSE OF ",reason, _evt);
3166 prevText = text;
3167 notify(ON.ON__TARGET_EDIT_TEXT_CHANGED, reason);
3168 }
3169 }
3170 $targetEdit.on('input change', function(evt){ _checkForChangedTargetText('input', evt); });
3171 // check for text which was inserted before this script had been initialized
3172 setTimeout(function(){_checkForChangedTargetText('initial');}, 250);
3173 checkForChangedTargetText = _checkForChangedTargetText;
3174}
3175
3176// [WebTranslator]
3177$rootContainer.find(".lmt__side_container .lmt__clear_text_button").click(function() { // Todo: -> translator.clearSourceText()
3178 const webTranslatorCore = webTranslator;
3179 const domElements = webTranslator.features.get('domElements');
3180
3181 webTranslatorCore.getActiveState().clearSentences();
3182 domElements.$targetEdit.val("");
3183 domElements.$translationsAsText.empty();
3184 resetSourceText(new RequestContext(webTranslatorCore.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED), "");
3185
3186 const event = document.createEvent('Event'); // clear the dictionary
3187 event.initEvent('change', true, true);
3188 domElements.$sourceEdit.get(0).dispatchEvent(event);
3189
3190 domElements.$sourceEdit.focus();
3191});
3192
3193
3194/** Prevent the passing on of scroll events beyond the container (and support scrolling momentum).
3195 Todo: Move to separate module
3196*/
3197config[CONFIG__BLOCK_SCROLLING] && (function(){
3198 let initiatingNode = undefined;
3199 let timeout = 0;
3200 let enableScrollLock = false;
3201 let prevDelta = 0;
3202 let dontPreventUpwardScrolling = false;
3203 let dontPreventDownwardScrolling = false;
3204 let lastEventTime = 0;
3205
3206 setTimeout(() => $(document).on('DOMMouseScroll mousewheel', '.lmt__scrollable, body', function(inEvent) {
3207 const theOriginalEvent = inEvent.originalEvent;
3208 const now = Date.now();
3209
3210 const wheelDelta = inEvent.type == 'DOMMouseScroll' ? theOriginalEvent.detail * -40 : theOriginalEvent.wheelDelta;
3211
3212 const $scrollableContainer = $(theOriginalEvent.target).closest('.lmt__scrollable');
3213 const scrolledNode = $scrollableContainer.length ? $scrollableContainer[0] : theOriginalEvent.target;
3214 const $scrolledNode = $(scrolledNode);
3215
3216 const timeSinceLastEvent = now - lastEventTime;
3217 lastEventTime = now;
3218
3219 if (timeSinceLastEvent>250 || Math.sign(wheelDelta)!==Math.sign(prevDelta)){
3220 // log("NEW SCROLLING ACTION");
3221
3222 if ($scrollableContainer.length && $scrollableContainer.scrollTop()<=3 /* && is not popup */){
3223 dontPreventUpwardScrolling = true;
3224 }else{
3225 dontPreventUpwardScrolling = false;
3226 }
3227 if ($scrollableContainer.length && $scrolledNode.scrollTop() >= $scrolledNode.prop("scrollHeight")-$scrolledNode.innerHeight()-1){
3228 dontPreventDownwardScrolling = true;
3229 }else {
3230 dontPreventDownwardScrolling = false;
3231 }
3232 }
3233
3234 if (scrolledNode===initiatingNode && now<timeout) // still the same scrolling action...
3235 timeout = now + 100;
3236
3237 const acceleration = Math.abs(wheelDelta) - Math.abs(prevDelta);
3238 prevDelta = wheelDelta;
3239
3240 if (now>timeout || acceleration>0){ // if no active scrolling action or a new one started (acceleration)
3241 initiatingNode = scrolledNode;
3242 timeout = now + 100;
3243
3244 if ($scrolledNode.hasClass('lmt__scrollable') && scrolledNode.scrollHeight > scrolledNode.clientHeight){
3245 enableScrollLock = true;
3246 }else {
3247 enableScrollLock = false;
3248 }
3249 }
3250 else if (scrolledNode!==initiatingNode){ // trying to scroll something else...
3251 timeout = now + 100;
3252 if (enableScrollLock || $scrolledNode.hasClass('lmt__scrollable')){ // don't propagate momentum from or to other scrollable nodes
3253 return preventPropagationForEvent(inEvent);
3254 }else return true;
3255 }
3256 if (enableScrollLock){
3257 const theTop = $scrolledNode.scrollTop();
3258 const theScrollHeight = $scrolledNode.prop("scrollHeight");
3259 const theHeight = $scrolledNode.innerHeight();
3260 let theResult = true;
3261
3262 if(wheelDelta < 0 && -wheelDelta > theScrollHeight - theHeight - theTop && !dontPreventDownwardScrolling) {
3263 $scrolledNode.scrollTop(theScrollHeight);
3264 theResult = preventPropagationForEvent(inEvent);
3265 }
3266 else if(wheelDelta > 0 && wheelDelta > theTop && !dontPreventUpwardScrolling) {
3267 $scrolledNode.scrollTop(0);
3268 theResult = preventPropagationForEvent(inEvent);
3269 }
3270 return theResult;
3271 }else{
3272 return true;
3273 }
3274 }));
3275})();
3276
3277
3278
3279/**
3280 * Observe pending requests
3281 *
3282 * @listens LMT_Event ON.ON__REQUEST_STARTED
3283 * @listens LMT_Event ON.ON__REQUEST_ENDED
3284 */
3285const hasPendingRequests = U.value(false);
3286webTranslator.hasPendingRequests = hasPendingRequests;
3287{
3288 let openRequests = 0;
3289 on(ON.ON__REQUEST_STARTED, _requestType => {
3290 ++openRequests;
3291 hasPendingRequests(openRequests>0);
3292 });
3293 on(ON.ON__REQUEST_ENDED, _requestType => {
3294 --openRequests;
3295 hasPendingRequests(openRequests>0);
3296 });
3297}
3298
3299/**
3300 * Show/hide busy indicator
3301 */
3302webTranslator.features.define('showBusyIndicator', ['webTranslatorCore', 'domElements'],
3303 function __feauture__selectLangUI(webTranslatorCore, domElements){
3304
3305 U.withValue(webTranslatorCore.hasPendingRequests, hasPendingRequests => {
3306 if (hasPendingRequests){
3307 domElements.rootContainer.classList.add('lmt--active_translation_request');
3308 } else {
3309 domElements.rootContainer.classList.remove('lmt--active_translation_request');
3310 }
3311 });
3312});
3313
3314
3315/**
3316 * If the source text gets empty -> fire ON.ON__SOURCE_TEXT_EMPTY
3317 *
3318 * @fires LMT_Event --> ON.ON__SOURCE_TEXT_EMPTY
3319 */
3320webTranslator.sourceIsEmpty.onValueChanged.push(function(isEmpty){
3321 isEmpty && notify(ON.ON__SOURCE_TEXT_EMPTY);
3322});
3323
3324
3325
3326
3327
3328const REGEX_noWordChar = /[\s.,:;!?"()*]/;
3329const REGEX_wordChar = /[^\s.,:;!?"()*]/;
3330
3331/***
3332 * Detect which target sentence has been clicked
3333 *
3334 * @listens targetEdit:click
3335 * @fires LMT_Event --> ON.ON__CLICK_TARGET_SENTENCE (state, click info (sentence, offset,...) or undefined)
3336*/
3337(function(){
3338 const dbg = false;
3339 function processClickEvent(click_left, click_top){
3340 if (webTranslator.features.get('smartTargetEditing').isPopupOpen()){
3341 webTranslator.features.get('smartTargetEditing').closePopup();
3342 return;
3343 }
3344 const state = webTranslator.getActiveState();
3345 var targetText = $targetEdit.val();
3346 webTranslator.features.get('updateTargetSentences').doRefreshTargetDataFromText(state, targetText);
3347 var cursorOffset = getTargetEditCursorOffset();
3348 var cursorOffset2 = $targetEdit.prop("selectionEnd");
3349 var selection = targetText.substr(cursorOffset, cursorOffset2-cursorOffset).trim();
3350 if (selection.indexOf(" ")!=-1){ // more than one word selected
3351 return;
3352 }
3353
3354 var wordOffset = cursorOffset;
3355 if (targetText.charAt(wordOffset).match(REGEX_wordChar)){ // move offset to word beginning
3356 while (wordOffset>0 && targetText.charAt(wordOffset-1).match(REGEX_wordChar)){
3357 --wordOffset;
3358 }
3359 }
3360
3361 var tSentencePos = getSentencePosForTargetCursorOffset(state, wordOffset);
3362
3363 if (tSentencePos){
3364
3365 if (targetText.substr(Math.max(0,cursorOffset-1), cursorOffset2-cursorOffset+2).trim().length==0 && tSentencePos.sentence[T_SENTENCE__TEXT].trim().length){ // clicked in an whitespace only area in a non-empty sentence
3366 dbg && console.log("[processClickEvent] Clicked in empty area.");
3367 return;
3368 }
3369
3370 var pixelOffset_before = getTargetEditPixelOffsetForCursorOffset(cursorOffset);
3371 var pixelOffset = getTargetEditPixelOffsetForCursorOffset(wordOffset);
3372 var pixelOffset2 = cursorOffset === cursorOffset2 ? pixelOffset : getTargetEditPixelOffsetForCursorOffset(cursorOffset2); // a word is selected (e.g. by double-click)
3373
3374 if (pixelOffset.top<=0 && pixelOffset.left<=0 && wordOffset>0){
3375 log("[processClickEvent] Unable to detect pixel position at offset ", wordOffset);
3376 return;
3377 }
3378
3379 // handle line breaks: the caret index is the same for "after the line" and "before the next line"; the following heuristic tries to detect if the user
3380 // clicked before the next line while the detected position is at the end of the last line.
3381 if (pixelOffset_before.left < pixelOffset.left-2){
3382 tSentencePos.caretAtNextLine = true;
3383 dbg && console.log("[processClickEvent] pixelOffset_before",pixelOffset_before, "pixelOffset",pixelOffset )
3384 } else {
3385 dbg && console.log("[processClickEvent] [" + targetText.charAt(cursorOffset-1) +targetText.charAt(cursorOffset) + targetText.charAt(cursorOffset+1) +"]");
3386 var pixelOffset_after = getTargetEditPixelOffsetForCursorOffset(cursorOffset + 1);
3387 var paddingLeft = parseInt($targetEdit.css("padding-left"));
3388 var lineHeight = parseInt($targetEdit.css("line-height"));
3389
3390 if (( (pixelOffset.top < pixelOffset2.top && targetText.charAt(cursorOffset-1)!=="\n") ||
3391 (click_left <= paddingLeft && targetText.charAt(cursorOffset-1)!=="\n") ||
3392 (click_left < pixelOffset_after.left && pixelOffset.top < pixelOffset_after.top)) &&
3393 wordOffset>0 && wordOffset<targetText.length-1){
3394 tSentencePos.caretAtNextLine = true;
3395 } else if (wordOffset>=targetText.length-1 && click_top > pixelOffset.top + lineHeight){
3396 tSentencePos = undefined;
3397 }
3398 }
3399 }
3400 dbg && console.log("[processClickEvent] Click on sentence:", cursorOffset, wordOffset, tSentencePos);
3401 window._activeTargetSentence = tSentencePos && tSentencePos.sentence;
3402 notify(ON.ON__CLICK_TARGET_SENTENCE, state, tSentencePos);
3403 }
3404
3405 if ( (navigator.vendor || "").match(/google/i)){ // chrome does not need a delay
3406 $targetEdit.on("click", function(evt){
3407 processClickEvent(evt.offsetX, evt.offsetY);
3408 });
3409 }else {
3410 let ignoreClicksUntil = 0;
3411 $targetEdit.on("click", function(evt){ // add a timeout to properly detect click events issued by touch events
3412 //console.log("--- click", getTargetEditCursorOffset(), evt);
3413 var now = Date.now();
3414 if (!DeviceProps.isIPad || ignoreClicksUntil < now){
3415 ignoreClicksUntil = now+50;
3416 var click_left = evt.offsetX;
3417 var click_top = evt.offsetY; // store the coordinates as the event becomes invalid
3418 setTimeout(function(){
3419 //console.log("--- click 2", getTargetEditCursorOffset(), evt);
3420 processClickEvent(click_left, click_top);
3421 });
3422 }
3423 });
3424 if (DeviceProps.isIPad){ // alternative click handling for iPad
3425 $targetEdit.on('touchend', function(evt){
3426 if (evt.originalEvent.targetTouches && !evt.originalEvent.targetTouches.length && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches.length){ // last finger removed
3427 var now = Date.now();
3428 if (ignoreClicksUntil < now){
3429 ignoreClicksUntil = now+50; // prevent double clicks (touch/click-event)
3430
3431 log("ipad touch-click");
3432
3433 if (webTranslator.features.get('smartTargetEditing').isPopupOpen()){
3434 webTranslator.features.get('smartTargetEditing').closePopup();
3435 return preventPropagationForEvent(evt);
3436 }
3437 var click_left = evt.layerX;
3438 var click_top = evt.layerY; // store the coordinates as the event may become invalid
3439 var cursorPos = getTargetEditCursorOffset();
3440 var numTries = 4;
3441 var _poll = function(){
3442 if (--numTries<0 || cursorPos!=getTargetEditCursorOffset()){ // cursor pos has finally updated
3443 processClickEvent(click_left, click_top);
3444 }else{
3445 setTimeout(_poll, 25);
3446 }
3447 }
3448 setTimeout(_poll, 100);
3449 }
3450 }
3451 });
3452 }
3453 }
3454})();
3455
3456
3457function MinorFailureReason(_msg){
3458 this.msg = _msg;
3459}
3460
3461function _updateTextareaText($textarea, newText){
3462 const f = _updateTextareaText;
3463 const textarea = $textarea[0];
3464 // on some browsers (e.g. Firefox), execCommand does not work with textareas. If this is detected, a fallback is used (undo does not work properly)
3465 if (config[CONFIG__UNDO] && (f._insertWorks || !f._checkedIfInsertWorks) ){
3466 const prevText = $textarea.val();
3467
3468 try{
3469 // log("[updateTextareaText] COMMAND", offset_oldPart_newPart, offset_oldPart_newPart[0] + offset_oldPart_newPart[1].length);
3470 textarea.focus();
3471 if (document.activeElement === textarea){
3472 const offset_oldPart_newPart = LMT_Utils.getStringChangeRange($textarea.val(), newText);
3473 textarea.setSelectionRange(offset_oldPart_newPart[0], offset_oldPart_newPart[0] + offset_oldPart_newPart[1].length);
3474 const execCommandEnabled = document.execCommand('insertText', false, offset_oldPart_newPart[2]);
3475
3476 // the webview on macos seems to lose execCommand after some time and the "alternatives" functionality stops working.
3477 // It's not clear if this loss happens only temporarily so we don't change _insertWorks for the time being.
3478 if ($textarea.val() !== newText) {
3479 if (!execCommandEnabled) {
3480 console.log('Textarea content is not equal to new text and execCommand is set to disabled.');
3481 } else {
3482 console.log('Textarea content is not equal to new text, but execCommand said it would work.');
3483 }
3484 $textarea.val(newText);
3485 }
3486 } else {
3487 log("[updateTextareaText] Can't select element.");
3488 $textarea.val(newText);
3489 }
3490 } finally{
3491 if (!f._checkedIfInsertWorks && prevText !== newText){
3492 f._checkedIfInsertWorks = true;
3493 if ($textarea.val() === newText){
3494 f._insertWorks = true;
3495 }
3496 else {
3497 f._insertWorks = false;
3498 $textarea.val(newText);
3499 log("[updateTextareaText] execCommand is not supported. Undo does not work!");
3500 }
3501 }
3502 }
3503 } else {
3504 $textarea.val(newText);
3505 }
3506
3507 // ... to trigger an immediate sourceIsEmpty detection (Don't use 'change'-Event to not trigger a re-translation)
3508 const event = document.createEvent('Event');
3509 event.initEvent('dl-programmatic-change', true, true);
3510 $textarea.get(0).dispatchEvent(event);
3511}
3512
3513
3514function updateTargetEditText(text){
3515 _updateTextareaText($targetEdit, text);
3516 checkForChangedTargetText('update');
3517}
3518
3519
3520
3521(function(){
3522 let latestKeyDownInfo;
3523 $targetEdit.on('keydown', function _targetEdit_keyDownHandler(evt){
3524 var info = {
3525 keyUpHandler: undefined,
3526 suppressKeyUpEvent: false,
3527 keyDownEvent: evt
3528 };
3529 latestKeyDownInfo = info;
3530 notify(ON.ON__TARGET_EDIT_KEY_DOWN, evt, info);
3531 });
3532 $targetEdit.on('keyup', function _targetEdit_keyDownHandler(evt){
3533 let info;
3534 if (latestKeyDownInfo && evt.keyCode === latestKeyDownInfo.keyDownEvent.keyCode){
3535 info = latestKeyDownInfo;
3536 }else {
3537 info = new Object(null);
3538 }
3539 if (info.keyUpHandler){
3540 return info.keyUpHandler(evt);
3541 }
3542 if (!info.suppressKeyUpEvent)
3543 notify(ON.ON__TARGET_EDIT_KEY_UP, evt, info);
3544
3545 });
3546})();
3547
3548// = == ============= smartTargetEditing =============== == =
3549
3550const TARGET_POPUP__SENTENCE = 'popupSentence';
3551const TARGET_POPUP__SENTENCE_CURSOR = 'popupSentenceCursor';
3552const TARGET_POPUP__GLOBAL_CURSOR = 'popupCursor';
3553const TARGET_POPUP__CARET_AT_NEXT_LINE = 'nextLineCursor';
3554const TARGET_POPUP__ONLY_WAITING_INDICATOR = 'waitingIndicator';
3555// const TARGET_POPUP__LOCAL_PREFIX = 'localPrefix';
3556const TARGET_POPUP__ENTRIES = 'entries';
3557const TARGET_POPUP__TYPE = 'isAc';
3558
3559
3560webTranslator.features.define('smartTargetEditing', ['domElements', 'webTranslatorCore', 'eventHandling', 'translationBackend', 'localizedTexts'],
3561 (domElements, webTranslatorCore, {on, ON, notify}, translationBackend, localizedTexts) => {
3562
3563 class TargetPopupInfo{
3564 constructor(){
3565 this[TARGET_POPUP__SENTENCE] = undefined;
3566 this[TARGET_POPUP__GLOBAL_CURSOR] = 0;
3567 this[TARGET_POPUP__SENTENCE_CURSOR] = 0;
3568 this[TARGET_POPUP__TYPE] = "alternatives";
3569 // this[TARGET_POPUP__LOCAL_PREFIX] = "";
3570 }
3571 }
3572
3573 const onTargetSentenceChangedByAlternative = U.createMultiProcedure();
3574 let $thePopup;
3575
3576 let messageBox;
3577 if (!DeviceProps.isMobilePhone){
3578 messageBox = LMT_UI_MessageBox.create('completionHint', localizedTexts.get('translator/messageBox_completionHint', "Choose a word and DeepL will finish the sentence for you."));
3579 messageBox.maximalDisplayCount(10);
3580 }
3581
3582 function updatePopupFrame() {
3583 if ($thePopup){
3584 const theOffset = $thePopup.offset();
3585 const theTop = theOffset.top - window.scrollY;
3586 const theLineHeight = parseInt($targetEdit.css("line-height"));
3587
3588 $thePopup.css("max-height", Math.min( theLineHeight * 10, Math.max($(window).height() - theTop - 4, theLineHeight * 3)));
3589
3590 if (messageBox){
3591 messageBox.height($thePopup.height())
3592 messageBox.position({
3593 top: theOffset.top,
3594 left: ((theOffset.left < 180 ? 180 : theOffset.left) + 11)
3595 }, "right");
3596 }
3597 }
3598 }
3599
3600 let mouseEnterTimeout = 0;
3601 function updatePopup(state, popupInfo){
3602 if (popupInfo[TARGET_POPUP__SENTENCE]===undefined || !webTranslatorCore.isActiveState(state)){
3603 if ($thePopup){
3604 $thePopup.remove();
3605 $thePopup = undefined;
3606 }
3607 return;
3608 }
3609
3610 if (!$thePopup){
3611 $thePopup = $("<div class='lmt__edit_text_popup lmt__scrollable' dl-test='translator-target-alternatives-popup'></div>")
3612 .addClass('lmt__edit_text_popup--visible')
3613 .insertAfter(domElements.targetEditOuterContainer);
3614 }
3615 const $popup = $thePopup;
3616
3617 let entries = [];
3618
3619 const offset = popupInfo[TARGET_POPUP__SENTENCE_CURSOR];
3620 const targetSentence = popupInfo[TARGET_POPUP__SENTENCE];
3621 const isAutoCompletion = popupInfo[TARGET_POPUP__TYPE] == "autocompletion";
3622 const isInitialAlternatives = popupInfo[TARGET_POPUP__TYPE] == "initialAlternatives";
3623 const isAlternatives = popupInfo[TARGET_POPUP__TYPE] == "alternatives";
3624
3625 if (popupInfo[TARGET_POPUP__ONLY_WAITING_INDICATOR]){
3626 $popup.empty().append("<div class='lmt__edit_text_popup__waiting_indicator'>...</div>");
3627 }
3628 else {
3629 entries = popupInfo[TARGET_POPUP__ENTRIES] || [];
3630 if (!entries.length){
3631 console.warn("[updatePopup]: no Translations for target sentence.", popupInfo);
3632 closePopup();
3633 return;
3634 }
3635 const prefix = targetSentence[T_SENTENCE__TEXT].substr(0, offset);
3636
3637 const $list = $("<ul></ul>");
3638
3639 const postfixes = {};
3640 entries.forEach(entry => {
3641 const postfix = repairText(entry.displayPostfix, state.getTargetLang());
3642 if (!postfixes[postfix]){
3643 postfixes[postfix] = true;
3644 $("<li></li>")
3645 .text(postfix)
3646 .on('mouseenter', function(){
3647 if (Date.now() > mouseEnterTimeout){ // if the popup is scrolled by a keyboard action, ignore the unwanted mouseenter event
3648 $popup.find('.lmt__edit_text_popup__entry--active').removeClass('lmt__edit_text_popup__entry--active');
3649 $(this).addClass('lmt__edit_text_popup__entry--active');
3650 }
3651 })
3652 .on('mouseleave', function() {
3653 if (isInitialAlternatives) $(this).removeClass('lmt__edit_text_popup__entry--active');
3654 })
3655 .appendTo($list)
3656 .on('click', function(_evt){
3657 if(window.getSelection().isCollapsed) {
3658 closePopup();
3659 const textBefore = targetSentence.getNormalizedText();
3660 insertTextIntoTargetSentence(state, targetSentence, prefix, entry.postfix, !isAutoCompletion)
3661 .then(function(){
3662 // place cursor behind inserted text
3663 const newCursorPos = popupInfo[TARGET_POPUP__GLOBAL_CURSOR] + entry.postfix.length;
3664 $targetEdit[0].setSelectionRange(newCursorPos, newCursorPos);
3665
3666 if (!isAutoCompletion){
3667 onTargetSentenceChangedByAlternative({
3668 sourceSentence: targetSentence.getSourceSentence(),
3669 targetPrefix: prefix,
3670 targetTextBefore: textBefore,
3671 targetTextAfter: targetSentence.getNormalizedText(),
3672 targetSentence: targetSentence,
3673 chosenAlternative: entry.postfix,
3674 })
3675 }
3676 },
3677 function _failed(lmtError){
3678 notify(ON.ON__REQUEST_FAILED, "fillUp", lmtError);
3679 });
3680 messageBox && messageBox.disable(true);
3681 }
3682 });
3683 }
3684 });
3685 $popup.empty().append($list);
3686
3687 //if (dlPageState) {
3688 // $popup.append("<span class='lmt__edit_text_popup__bg'></span>")
3689 // $popup.append("<span class='lmt__edit_text_popup__top_peak'></span>")
3690 //}
3691 }
3692 if (isAutoCompletion) {
3693 $popup
3694 .addClass("lmt__edit_text_popup--autocompletion")
3695 .removeClass("lmt__edit_text_popup--initial_alternatives")
3696 .removeClass("lmt__edit_text_popup--alternatives");
3697 }
3698 else if (isInitialAlternatives) {
3699 $popup
3700 .removeClass("lmt__edit_text_popup--autocompletion")
3701 .addClass("lmt__edit_text_popup--initial_alternatives")
3702 .removeClass("lmt__edit_text_popup--alternatives");
3703 }
3704 else if (isAlternatives) {
3705 $popup
3706 .removeClass("lmt__edit_text_popup--autocompletion")
3707 .removeClass("lmt__edit_text_popup--initial_alternatives")
3708 .addClass("lmt__edit_text_popup--alternatives");
3709 }
3710
3711 const fontSize = parseInt($targetEdit.parent().css('font-size'));
3712 $popup.css('font-size', (fontSize<18) ? fontSize+"px" : "");
3713 const lineHeight = parseFloat($targetEdit.css("line-height")) ||
3714 parseFloat($targetEdit.css("font-size")) * 1.2; // in case of 'line-height' is 'normal'
3715
3716 const pixelPos = getTargetEditPixelOffsetForCursorOffset(popupInfo[TARGET_POPUP__GLOBAL_CURSOR]);
3717 if (popupInfo[TARGET_POPUP__CARET_AT_NEXT_LINE]){
3718 pixelPos.left = parseInt($targetEdit.css("padding-left"));
3719 pixelPos.top += lineHeight;
3720 }
3721
3722 pixelPos.left -= 4;
3723
3724 pixelPos.top = Math.round(pixelPos.top);
3725 pixelPos.left = Math.round(pixelPos.left);
3726 const popupGlobalAnchorPos = $popup.offsetParent().offset();
3727 const editGlobalPos = $targetEdit.offset();
3728
3729 pixelPos.top += editGlobalPos.top - popupGlobalAnchorPos.top;
3730 pixelPos.left += editGlobalPos.left - popupGlobalAnchorPos.left;
3731
3732 const delta_y = lineHeight; // * 0.8;
3733 const popupTop = pixelPos.top + delta_y;
3734 let popupLeft = pixelPos.left;
3735 const popupWidth = $popup.outerWidth();
3736 const scrollX = window.scrollX || 0;
3737
3738 if (popupLeft+popupWidth>$(window).width() + scrollX - popupGlobalAnchorPos.left-10){
3739 popupLeft = $(window).width() + scrollX - popupGlobalAnchorPos.left-popupWidth-10;
3740 }
3741 $popup.scrollTop(0);
3742 $popup.css("transform","translate(" + popupLeft + "px,"+(popupTop)+"px)");
3743 updatePopupFrame();
3744 if (messageBox && entries.length > 1 && popupInfo[TARGET_POPUP__TYPE] == "alternatives") {
3745 messageBox.show(250);
3746 }
3747 popupInfo.pixelOffset = {
3748 left: popupLeft,
3749 top: popupTop,
3750 }
3751 notify(ON.ON__TARGET_POPUP_OPENED, popupInfo);
3752 /* localisation.js i.d.R. noch nicht geladen -> String statt Konstante */
3753 }
3754 function closePopup(){
3755 const popupInfo = new TargetPopupInfo;
3756 popupInfo[TARGET_POPUP__SENTENCE] = undefined;
3757 updatePopup(webTranslatorCore.getActiveState(), popupInfo);
3758 messageBox && messageBox.hide(250);
3759 notify(ON.ON__TARGET_POPUP_CLOSED);
3760 }
3761 function isPopupOpen(){
3762 return !!$thePopup;
3763 }
3764
3765 function requestAlternativesForPopup(state, popupInfo){
3766 const dbg = false;
3767 const TAG = "[requestAlternativesForPopup]";
3768 return new Promise(function(success, failure){
3769
3770 /** @type LMT_TargetSentence */
3771 const targetSentence = popupInfo[TARGET_POPUP__SENTENCE];
3772 if (popupInfo[TARGET_POPUP__SENTENCE]===undefined || !webTranslatorCore.isActiveState(state)){
3773 failure(new MinorFailureReason());
3774 return;
3775 }
3776
3777 const translationObj = targetSentence.getTranslation();
3778 if (!translationObj){
3779 dbg && console.log(TAG, " No Translation object.");
3780 failure(new MinorFailureReason());
3781 return;
3782 }
3783
3784 dbg && console.log(TAG, "THE TEXT [" + targetSentence[T_SENTENCE__TEXT] + "]", "offset:", popupInfo[TARGET_POPUP__SENTENCE_CURSOR],
3785 "["+targetSentence[T_SENTENCE__TEXT].substr(0,popupInfo[TARGET_POPUP__SENTENCE_CURSOR])+"]");
3786
3787 const [normalizedText, normalizedSentenceCursorOffset] = targetSentence.getNormalizedTextAndOffset(popupInfo[TARGET_POPUP__SENTENCE_CURSOR]);
3788
3789 const prefix = normalizedText.substr(0, normalizedSentenceCursorOffset);
3790 const prefixLength = prefix.length;
3791 const existingPostfix = normalizedText.substr(normalizedSentenceCursorOffset);
3792
3793 dbg && console.log(TAG, "THE PREFIX [" + prefix + "]", "normalizedOffset:",normalizedSentenceCursorOffset);
3794
3795 // don't open popup behind a sentence
3796 if (existingPostfix.trim().length==0 && targetSentence[T_SENTENCE__LAST_FULL_TEXT].length>3 &&
3797 prefix.trim().substr(prefix.trim().length-5) === targetSentence[T_SENTENCE__LAST_FULL_TEXT].substr(targetSentence[T_SENTENCE__LAST_FULL_TEXT].length-5)){
3798 dbg && console.log(TAG, "Don't update popup behind sentence");
3799 success([]);
3800 return;
3801 }
3802 //dbg && console.log(TAG, "THE POSTFIX [" + existingPostfix + "]");
3803
3804
3805 const filterEntries = (entries) => { // skip empty entries and entries that would not change the existing sentence.
3806 const filteredEntries = [];
3807 dbg && console.log(TAG, "existingPostfix:", existingPostfix);
3808 entries.forEach(function(entry){
3809 const postfix = entry.postfix;
3810 if (postfix.trim().length && !existingPostfix.startsWith(postfix)){
3811 filteredEntries.push(entry);
3812 }
3813 });
3814 return filteredEntries;
3815 }
3816
3817 // cached results.
3818 if (translationObj[TRANSLATION__PREFIXES_WITH_REQUESTED_LOCAL_ALTERNATIVES][prefix]){
3819 dbg && console.log(TAG, "Already requested.");
3820 success(filterEntries(translationObj[TRANSLATION__PREFIXES_WITH_REQUESTED_LOCAL_ALTERNATIVES][prefix]));
3821 return;
3822 }
3823
3824 dbg && console.log(TAG, "[" + translationObj[TRANSLATION__SOURCE_TEXT] + "]->[" + prefix + "]");
3825
3826 const job = {
3827 'kind': JOB_KIND__ALTERNATIVES_AT_POSITION,
3828 'de_sentence_beginning': prefix,
3829 'raw_en_sentence': translationObj[TRANSLATION__SOURCE_TEXT]
3830 };
3831 if (state.isContextEnabled()){
3832 const sourceSentence = targetSentence.getSourceSentence();
3833 if (sourceSentence){
3834 const context = doGetSourceContext(sourceSentence);
3835 job['raw_en_context_before'] = context.prefix;
3836 job['raw_en_context_after'] = context.postfix;
3837 }
3838 }
3839 translationBackend.sendTranslationJobs([job], state.createJobSettings(), false).then(translationResults => {
3840 notify(ON.ON__RPC_CALL_SUCCESSFULL, "alternatives");
3841 const translations = translationResults[TRANSLATION_RESULT__TRANSLATIONS];
3842 const records = ((translations || [])[0] || [])['beams'] || [];
3843 const entries = [];
3844 records.forEach(function(records){
3845 const receivedPrefix = records[RECORD__POSTPROCESSED_SENTENCE].substr(0, prefixLength);
3846 if (prefix.trim()!==receivedPrefix.trim()){
3847 console.warn(TAG, "Prefix does not match\nreceived ["+receivedPrefix+"]\n expected [" +prefix + "]");
3848 }else{
3849 const postfix = records[RECORD__POSTPROCESSED_SENTENCE].substr(prefixLength);
3850 entries.push({
3851 displayPostfix: postfix + " ...",
3852 postfix: postfix
3853 });
3854 }
3855 });
3856 translationObj[TRANSLATION__PREFIXES_WITH_REQUESTED_LOCAL_ALTERNATIVES][prefix] = entries;
3857 const filteredEntries = filterEntries(entries);
3858 if (!filterEntries.length) {
3859 notify(ON.ON__NO_ALTERNATIVES_FOUND);
3860 }
3861 success(filteredEntries); // the entries...
3862 }, lmtError => {
3863 notify(ON.ON__REQUEST_FAILED, "alternatives", lmtError);
3864 failure(lmtError);
3865 });
3866
3867 });
3868 }
3869 function repairText(inText, langCode) {
3870 let text = inText;
3871 if (langCode=="DE" && !queryVars["norepair"]){
3872 text = text.replace("/ä", "ä")
3873 .replace("Ä","Ä")
3874 .replace("ö","ö")
3875 .replace("Ö","Ö")
3876 .replace("ü","ü")
3877 .replace("Ü","Ü")
3878 .replace("ß", "ß")
3879 .replace("Ã? ","ü")
3880 .replace("Ã?","ü")
3881 .replace("ÃŒ","ü")
3882 .replace("Ã","");
3883 }
3884 if (text !== inText)
3885 console.log("repaired text from \""+inText+"\" to \""+text+"\"");
3886 return text;
3887 }
3888
3889 function updateTargetEditFromState(state){
3890 if (webTranslator.isActiveState(state)){
3891 const s = [];
3892 state[STATE__TARGET_SENTENCES].forEach(function(targetSentence){
3893 const t = targetSentence[T_SENTENCE__TEXT];
3894 s.push(t);
3895 });
3896 const targetText = s.join("");
3897 state[STATE__TARGET_TEXT] = targetText;
3898 updateTargetEditText(targetText);
3899 }
3900 }
3901 function request_fillUpTargetSentence(state, targetSentence, _postfixWhitespaces){
3902 return new Promise(function(success, failure){
3903 // log("[request_fillUpTargetSentence]");
3904 var normalizedText = targetSentence.getNormalizedTextAndOffset(0)[0];
3905
3906 // log("[request_fillUpTargetSentence] normalized text: ["+normalizedText+"]");
3907 var parts = targetSentence.getParts();
3908 var whitespacesBefore = "";
3909 if (parts.length && parts[0].isWhitespacePart){
3910 whitespacesBefore = parts[0].text;
3911 }
3912 var whitespacesAfter = _postfixWhitespaces===undefined ? " " : _postfixWhitespaces; // ????
3913
3914 var prefix = normalizedText;
3915 log("[request_fillUpTargetSentence] [" + whitespacesBefore + "][" + prefix + "][...]["+whitespacesAfter+"]");
3916
3917 var translationObj = targetSentence.getTranslation();
3918 if (!translationObj){
3919 log("[request_fillUpTargetSentence] No Translation object.");
3920 return;
3921 }
3922 // log(translationObj[TRANSLATION__SOURCE_TEXT])
3923
3924 var job = {
3925 'kind': JOB_KIND__DEFAULT,
3926 'de_sentence_beginning': prefix,
3927 'raw_en_sentence': translationObj[TRANSLATION__SOURCE_TEXT]
3928 };
3929 if (state.isContextEnabled()){
3930 var sourceSentence = targetSentence.getSourceSentence();
3931 if (sourceSentence){
3932 var context = doGetSourceContext(sourceSentence);
3933 job['raw_en_context_before'] = context.prefix;
3934 job['raw_en_context_after'] = context.postfix;
3935 }
3936 }
3937 // var translationsToUpdate = [translationObj]; // ?????????????????? Todo: (Claudius) This shouldn't be needed anymore?!
3938
3939 notify(ON.ON__REQUEST_STARTED, "fillUp");
3940 webTranslator.translationBackend.sendTranslationJobs([job], state.createJobSettings(), true).then(function(translationResults){
3941 notify(ON.ON__RPC_CALL_SUCCESSFULL, "fillUp");
3942 notify(ON.ON__REQUEST_ENDED, "fillUp");
3943 var records = translationResults[TRANSLATION_RESULT__TRANSLATIONS][0]['beams'];
3944 if (records.length){
3945 var newText = whitespacesBefore + repairText(records[0]['postprocessed_sentence'], state.getTargetLang()) + whitespacesAfter;
3946 targetSentence.setText(newText, true);
3947 targetSentence[T_SENTENCE__EDIT_REV] = counter();
3948 }else{
3949 log("request_fillUpTargetSentence: No Result found");
3950 }
3951 updateTargetEditFromState(state);
3952 success();
3953 }, function(lmtError){
3954 notify(ON.ON__REQUEST_ENDED, "fillUp");
3955 failure(lmtError);
3956 });
3957 });
3958 }
3959
3960 function insertTextIntoTargetSentence(state, targetSentence, prefix, rawInsertedText, shouldRequestSentenceCompletion){
3961 return new Promise(function(success, failure){
3962 var insertedText = repairText(rawInsertedText, state.getTargetLang());
3963
3964 var postfixWhitespaces = targetSentence[T_SENTENCE__TEXT].substr(prefix.length).match(/\s*$/)[0];
3965 dbg && console.log("[insertTextIntoTargetSentence] [" + prefix + "][" + insertedText + "]", "whitespaces["+postfixWhitespaces+"]");
3966
3967
3968 dlStats.countStats("e", function(x) { return x+1 }, 0);
3969 dlStats.countEvent("target_alternative_selected",1);
3970
3971 targetSentence.setText(prefix + insertedText + postfixWhitespaces, false); // tmp fix to keep the post-whitespaces during request
3972 targetSentence[T_SENTENCE__EDIT_REV] = counter();
3973
3974 updateTargetEditFromState(state);
3975
3976 if (shouldRequestSentenceCompletion){// Send request to fill end of sentence
3977 targetSentence.setText(prefix + insertedText, false);
3978 targetSentence[T_SENTENCE__EDIT_REV] = counter();
3979
3980 notify(ON.ON__USER_EDIT_PERFORMED, {
3981 state,
3982 targetSentence,
3983 type: 'EDIT_TYPE__SELECT_ALTERNATIVE_FIRST_STEP'
3984 });
3985
3986 request_fillUpTargetSentence(state, targetSentence, postfixWhitespaces).then(function _success(_result){
3987 U.notify('LMT/translationChanged'); // deprecated
3988
3989 notify(ON.ON__USER_EDIT_PERFORMED, {
3990 state,
3991 targetSentence,
3992 type: 'EDIT_TYPE__SELECT_ALTERNATIVE'
3993 });
3994
3995 success();
3996 }, failure);
3997 } else {
3998 targetSentence.setText(prefix + insertedText, true);
3999
4000 U.notify('LMT/translationChanged'); // deprecated
4001
4002 notify(ON.ON__USER_EDIT_PERFORMED, {
4003 state,
4004 targetSentence,
4005 type: 'EDIT_TYPE__AUTOCOMPLETION'
4006 });
4007 success();
4008 }
4009 });
4010 }
4011
4012 // init
4013 $(window).resize(() => updatePopupFrame());
4014
4015 domElements.$sourceEdit.on('focus', () => closePopup());
4016 domElements.$targetEdit.parent().on('scroll', () => closePopup() );
4017 // on(ON.ON__SOURCE_TEXT_CHANGED, close);
4018
4019 // todo: this is the wrong place for this code:
4020 domElements.$targetEdit.on("focus", () => {
4021 // $targetEdit.hide_additional_translations = true;
4022 $translationsAsText.html("<div class='lmt__busy_indicator'></div>");
4023 $rootContainer.removeClass("lmt--showing_alternatives");
4024 });
4025
4026 $('body').on('click', evt => {
4027 if (!$(evt.target).closest(".lmt__edit_text_popup").length && !$(evt.target).closest(".lmt__target_textarea").length)
4028 closePopup();
4029 });
4030
4031 on(ON.ON__REQUEST_FAILED, (_requestType, _lmtError) => {
4032 closePopup();
4033 setTimeout(() => closePopup(), 150);
4034 });
4035
4036
4037 /** ? [ON.ON__CLICK_TARGET_SENTENCE]
4038 - Get the clicked sentence
4039 - Update the popup
4040 */
4041 let _tDelay;
4042 on(ON.ON__CLICK_TARGET_SENTENCE, function _updateAndOpenPopup(state, tSentenceCursor){
4043 if (globalState[GSTATE__EDIT_FEATURES_DISABLED]()){
4044 closePopup();
4045 }
4046 else if (!tSentenceCursor){
4047 if (webTranslatorCore.isActiveState(state)){
4048 closePopup();
4049 }
4050 }
4051 else{
4052 const popupInfo = new TargetPopupInfo();
4053 popupInfo[TARGET_POPUP__SENTENCE] = tSentenceCursor.sentence;
4054 popupInfo[TARGET_POPUP__GLOBAL_CURSOR] = tSentenceCursor.cursorOffset;
4055 popupInfo[TARGET_POPUP__SENTENCE_CURSOR] = tSentenceCursor.sentenceCursorOffset;
4056 popupInfo[TARGET_POPUP__CARET_AT_NEXT_LINE] = tSentenceCursor.caretAtNextLine;
4057
4058 if (webTranslatorCore.isActiveState(state)){
4059 popupInfo[TARGET_POPUP__ONLY_WAITING_INDICATOR] = true;
4060 notify(ON.ON__REQUEST_STARTED, "clickPopupRequest");
4061 _tDelay = setTimeout(() => updatePopup(state, popupInfo), 50); // ?? what is the purpose of the timeout ??
4062 requestAlternativesForPopup(state, popupInfo).then(entries => {
4063 notify(ON.ON__REQUEST_ENDED, "clickPopupRequest");
4064 clearTimeout(_tDelay);
4065 popupInfo[TARGET_POPUP__ONLY_WAITING_INDICATOR] = false;
4066 popupInfo[TARGET_POPUP__ENTRIES] = entries;
4067 updatePopup(state, popupInfo);
4068 }, lmtError => {
4069 notify(ON.ON__REQUEST_ENDED, "clickPopupRequest");
4070 closePopup();
4071 console.warn("================","\n",lmtError);
4072 });
4073 }
4074 }
4075 });
4076
4077 function openAutocompletion(){
4078 const dbg = false;
4079 const TAG = "[autocompletion]";
4080 const state = webTranslatorCore.getActiveState();
4081
4082 let targetSentence;
4083 let initialCursorOffset;
4084 let prefix;
4085 const onRequestEnded = U.createCallOnce();
4086
4087 U.scheduleNonConcurrentRequest("onKeyRequest_" + state.getId(), () => {
4088 return new Promise(function(success, failure){
4089
4090 if (!webTranslatorCore.isActiveState(state)){
4091 log(TAG, "Outdated.");
4092 failure(new MinorFailureReason());
4093 return;
4094 }
4095 initialCursorOffset = getTargetEditCursorOffset();
4096 const tSentencePos = getSentencePosForTargetCursorOffset(state, initialCursorOffset);
4097
4098 dbg && console.log(TAG, "initialCursorOffset",initialCursorOffset );
4099 dbg && console.log(TAG, "tSentencePos",tSentencePos );
4100
4101 if (!tSentencePos){
4102 log(TAG, "No aligned sentence found.");
4103 failure(new MinorFailureReason());
4104 return;
4105 }
4106 notify(ON.ON__REQUEST_STARTED, "autocompletion");
4107 onRequestEnded.push(function(){ notify(ON.ON__REQUEST_ENDED, "autocompletion"); });
4108 targetSentence = tSentencePos.sentence;
4109 const offset = tSentencePos.sentenceCursorOffset;
4110
4111 const [normalizedText, normalizedOffset] = targetSentence.getNormalizedTextAndOffset(offset);
4112
4113 prefix = normalizedText.substr(0, normalizedOffset);
4114 const translationObj = targetSentence.getTranslation();
4115
4116 dbg && console.log(TAG, "prefix ["+prefix+"]");
4117
4118 // disable autocompletion behind a known full sentence
4119 dbg && console.log(TAG, "T_SENTENCE__LAST_FULL_TEXT", targetSentence[T_SENTENCE__LAST_FULL_TEXT]);
4120 if (targetSentence[T_SENTENCE__LAST_FULL_TEXT] &&prefix.startsWith(targetSentence[T_SENTENCE__LAST_FULL_TEXT].trim())){
4121 dbg && console.log(TAG, "prevent behind sentence.");
4122 success([]);
4123 return;
4124 }
4125
4126 let cachedResult;
4127
4128 const cachedResultsToCheck = targetSentence[T_SENTENCE__AUTOCOMPLETION_RESULTS].slice(0);
4129
4130 const initialResult = targetSentence.getTranslation()[TRANSLATION__INITIAL_TRANSLATION_RESULT]; // add the initial sentence as a possible autocompletion result.
4131 if (initialResult){
4132 const fakeACResult = new Object(null);
4133 fakeACResult[TRANSLATION_RESULT__TRANSLATIONS] = [initialResult];
4134 cachedResultsToCheck.push(["", targetSentence.getTranslation()[TRANSLATION__INITIAL_TRANSLATION_TEXT], fakeACResult]);
4135 }
4136
4137 cachedResultsToCheck.some(function(requestedPrefix_fullSentence_result){
4138 const requestedPrefix = requestedPrefix_fullSentence_result[0], fullSentence = requestedPrefix_fullSentence_result[1], response = requestedPrefix_fullSentence_result[2];
4139 if (prefix.startsWith(requestedPrefix) && fullSentence.startsWith(prefix)){
4140 //log(TAG, "cache! ");
4141 cachedResult = response;
4142 dbg && console.log(TAG, "cache! ", requestedPrefix_fullSentence_result);
4143 return true;
4144 }
4145 });
4146
4147 if (cachedResult){
4148 success(cachedResult);
4149 return;
4150 }
4151
4152 dbg && console.log(TAG, "parts:", targetSentence.getParts());
4153 dbg && console.log(TAG, "cursorOffset:", initialCursorOffset, "normalized:",normalizedOffset);
4154
4155 const job = {
4156 'kind': JOB_KIND__AUTOCOMPLETION,
4157 'de_sentence_beginning': prefix,
4158 'raw_en_sentence': translationObj[TRANSLATION__SOURCE_TEXT]
4159 };
4160 if (state.isContextEnabled()){
4161 const sourceSentence = targetSentence.getSourceSentence();
4162 if (sourceSentence){
4163 const context = doGetSourceContext(sourceSentence);
4164 job['raw_en_context_before'] = context.prefix;
4165 job['raw_en_context_after'] = context.postfix;
4166 }
4167 }
4168
4169 dbg && console.log(TAG, "send request ");
4170 translationBackend.sendTranslationJobs([job], state.createJobSettings(), false).then(result => {
4171 notify(ON.ON__RPC_CALL_SUCCESSFULL, "autocomplete");
4172 success(result);
4173 }, failure);
4174
4175 });
4176 }, 1000).then(translationResults => {
4177 onRequestEnded();
4178
4179 dbg && console.log("[autcompletion] initialCursorOffset:", initialCursorOffset);
4180 const tSentencePos = getSentencePosForTargetCursorOffset(state, initialCursorOffset);
4181 if (tSentencePos && tSentencePos.sentence===targetSentence){ // still the same sentence
4182 dbg && console.log("[autcompletion] Results:", translationResults);
4183
4184 if (!tSentencePos){
4185 dbg && console.log(TAG, "No aligned sentence found.");
4186 return;
4187 }
4188 // search beginning of word
4189 let localPrefix = "";
4190 const sentenceText = targetSentence[T_SENTENCE__TEXT];
4191 let sentenceOffset = tSentencePos.sentenceCursorOffset;
4192 if (sentenceOffset>0){
4193 --sentenceOffset;
4194
4195 while (sentenceOffset>=0 && !sentenceText.charAt(sentenceOffset).match(REGEX_noWordChar)){
4196 localPrefix = sentenceText.charAt(sentenceOffset) + localPrefix;
4197 --sentenceOffset;
4198 --tSentencePos.sentenceCursorOffset;
4199 --tSentencePos.cursorOffset;
4200 }
4201 }
4202 const pixelPos1 = getTargetEditPixelOffsetForCursorOffset(tSentencePos.cursorOffset);
4203 const pixelPos2 = getTargetEditPixelOffsetForCursorOffset(tSentencePos.cursorOffset+1);
4204 dbg && console.log(TAG, "local prefix ["+localPrefix+"]", pixelPos1.left>pixelPos2.left && pixelPos1.top<pixelPos2.top+4, pixelPos1, pixelPos2);
4205
4206 const popupInfo = new TargetPopupInfo();
4207 popupInfo[TARGET_POPUP__SENTENCE] = targetSentence;
4208 popupInfo[TARGET_POPUP__SENTENCE_CURSOR] = tSentencePos.sentenceCursorOffset;
4209 popupInfo[TARGET_POPUP__GLOBAL_CURSOR] = tSentencePos.cursorOffset;
4210 popupInfo[TARGET_POPUP__CARET_AT_NEXT_LINE] = pixelPos1.left>pixelPos2.left && pixelPos1.top<pixelPos2.top+4;
4211 popupInfo[TARGET_POPUP__TYPE] = "autocompletion";
4212 //popupInfo[TARGET_POPUP__LOCAL_PREFIX] = localPrefix;
4213
4214 const translations = translationResults[TRANSLATION_RESULT__TRANSLATIONS];
4215 const records = ((translations || [])[0] || [])['beams'] || [];
4216 const prefixLength = prefix.trimStart().length;
4217
4218 const entries = [];
4219 if (records.length){
4220 const record = records[0];
4221 const postfix = record[RECORD__POSTPROCESSED_SENTENCE].substr(prefixLength);
4222 const text = localPrefix + postfix;
4223 if (text.trim().length){
4224 entries.push({
4225 displayPostfix: text,
4226 postfix: text
4227 });
4228 const cacheEntry = [prefix, record[RECORD__POSTPROCESSED_SENTENCE], translationResults];
4229 if (!targetSentence[T_SENTENCE__AUTOCOMPLETION_RESULTS].some(function(requestedPrefix_fullSentence_result){
4230 return requestedPrefix_fullSentence_result[0] === cacheEntry[0] && requestedPrefix_fullSentence_result[1] === cacheEntry[1];
4231 })){
4232 targetSentence[T_SENTENCE__AUTOCOMPLETION_RESULTS].push(cacheEntry);
4233 }
4234 }
4235 }
4236 popupInfo[TARGET_POPUP__ENTRIES] = entries;
4237
4238 updatePopup(state, popupInfo);
4239 }else{
4240 log(TAG, "canceled.", tSentencePos, targetSentence);
4241 }
4242
4243 }, cancel_or_lmtError => {
4244 onRequestEnded();
4245 if (cancel_or_lmtError===U.scheduleNonConcurrentRequest.FAILURE_REASON__CANCELED){
4246 dbg && console.log(TAG, " CANCELED");
4247 }
4248 else{
4249 notify(ON.ON__REQUEST_FAILED, "autocompletion", cancel_or_lmtError);
4250 console.error(cancel_or_lmtError);
4251 }
4252 // ignore
4253 });
4254 }
4255
4256 if (DeviceProps.isIPad){ // alternative click handling for iPad
4257 $('body').on('touchend', function(evt){
4258 if (evt.originalEvent.targetTouches && !evt.originalEvent.targetTouches.length && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches.length){ // last finger removed
4259 if (!$(evt.target).closest(".lmt__edit_text_popup").length && !$(evt.target).closest(".lmt__target_textarea").length){
4260 log("[popup] close by touch");
4261 closePopup();
4262 }
4263 }
4264 });
4265 }
4266
4267
4268 on(ON.ON__TARGET_EDIT_KEY_DOWN, function(evt, info){
4269 let preventDefault = false;
4270 if (isPopupOpen()){
4271 const $popup = $thePopup;
4272
4273 const activatePopupEntry = $entry => {
4274 $popup.find('.lmt__edit_text_popup__entry--active').removeClass('lmt__edit_text_popup__entry--active');
4275 $entry.addClass('lmt__edit_text_popup__entry--active');
4276 if ($entry.length){
4277 const pos = $entry.position();
4278 const pHeight = $popup.height();
4279 const entryHeight = $entry.height();
4280 if (pos.top<=entryHeight){
4281 mouseEnterTimeout = Date.now() + 250; // ignore unwanted mouseenter events fired by scrolling
4282 $popup.scrollTop( $popup.scrollTop() + pos.top - entryHeight );
4283 }else if (pos.top>=pHeight-entryHeight*3){
4284 mouseEnterTimeout = Date.now() + 250; // ignore unwanted mouseenter events fired by scrolling
4285 $popup.scrollTop( $popup.scrollTop() + pos.top-(pHeight-entryHeight*3) );
4286 }
4287 }
4288 }
4289
4290 if (evt.keyCode===KEY__ESC){
4291 info.suppressKeyUpEvent = true;
4292 closePopup();
4293 }
4294 else if (evt.keyCode===KEY__DOWN){
4295 const $entry = $popup.find('.lmt__edit_text_popup__entry--active');
4296 if ($entry.length){
4297 const $next = $entry.next("li");
4298 if ($next.length){
4299 activatePopupEntry($next);
4300 }
4301 }
4302 else{
4303 $popup.find('li').first().addClass('lmt__edit_text_popup__entry--active');
4304 }
4305 preventDefault = true;
4306 }
4307 else if (evt.keyCode===KEY__UP){
4308 const $entry = $popup.find('.lmt__edit_text_popup__entry--active');
4309 if ($entry.length){
4310 const $prev = $entry.prev("li");
4311 if ($prev.length){
4312 activatePopupEntry($prev);
4313 }
4314 else{
4315 closePopup();
4316 }
4317 preventDefault = true;
4318 }
4319 else{
4320 closePopup();
4321 }
4322 }
4323 else if (evt.keyCode===KEY__ENTER){
4324 const $entry = $popup.find('.lmt__edit_text_popup__entry--active');
4325 if ($entry.length){
4326 $entry.click();
4327 }
4328 closePopup();
4329 preventDefault = true;
4330 }
4331 else{
4332 closePopup();
4333 }
4334 if (preventDefault){
4335 preventPropagationForEvent(evt);
4336 info.keyUpHandler = function(evt){
4337 return preventPropagationForEvent(evt);
4338 };
4339 info.suppressKeyUpEvent = true;
4340 }
4341 }
4342 });
4343
4344 /** ? targetEdit -> keyup
4345 - update autocompletion
4346 */
4347 on(ON.ON__TARGET_EDIT_KEY_UP, function _openEditPopup(evt, _info){
4348 function triggerAlternativePopup(){
4349 const state = webTranslatorCore.getActiveState();
4350 const targetText = $targetEdit.val();
4351 webTranslator.features.get('updateTargetSentences').doRefreshTargetDataFromText(state, targetText);
4352 const cursorOffset = getTargetEditCursorOffset();
4353
4354 let wordOffset = cursorOffset;
4355 if (targetText.charAt(wordOffset).match(REGEX_wordChar)){ // move offset to word beginning
4356 while (wordOffset>0 && targetText.charAt(wordOffset-1).match(REGEX_wordChar)){
4357 --wordOffset;
4358 }
4359 }
4360 const tSentencePos = getSentencePosForTargetCursorOffset(state, wordOffset);
4361 if (tSentencePos){
4362 const pixelOffset1 = getTargetEditPixelOffsetForCursorOffset(wordOffset);
4363 const pixelOffset2 = getTargetEditPixelOffsetForCursorOffset(wordOffset + 1);
4364 if (pixelOffset2.top > pixelOffset1.top + 5){
4365 tSentencePos.caretAtNextLine = true;
4366 }
4367 }
4368 log("[triggerAlternativePopup] Click on sentence:", cursorOffset, wordOffset, tSentencePos);
4369 notify(ON.ON__CLICK_TARGET_SENTENCE, state, tSentencePos);
4370 }
4371
4372 if (evt.keyCode === KEY__SPACE){
4373 triggerAlternativePopup();
4374 }
4375 else if (evt.keyCode===KEY__LEFT || evt.keyCode===KEY__RIGHT
4376 || evt.keyCode===KEY__HOME || evt.keyCode===KEY__END
4377 || evt.keyCode===KEY__DOWN || evt.keyCode===KEY__UP) {
4378 closePopup();
4379 U.setTimeout_consolidated("triggerAlternativePopup", function(){
4380 if (document.activeElement == $targetEdit[0]){
4381 const offset = getTargetEditCursorOffset();
4382 const charBefore = $targetEdit.val().charAt(offset-1);
4383 const char = $targetEdit.val().charAt(offset);
4384 if ( (offset==0 || charBefore==="\n" || charBefore===" ") && char!==" " && char !=="\n" )// only trigger popup at beginning of word
4385 triggerAlternativePopup();
4386 }
4387 }, 300);
4388 }
4389 else if( (evt.keyCode>=15 && evt.keyCode<32) || evt.keyCode===224 || evt.keyCode === KEY__PG_UP ||
4390 evt.keyCode === KEY__PG_DOWN || evt.keyCode === KEY__TAB){ // exclude some meta-keys
4391 // ignore
4392 }
4393 else{
4394 // console.log("[KEY UP]", evt.keyCode);
4395 webTranslator.features.get('updateTargetSentences').doRefreshTargetDataFromText(webTranslatorCore.getActiveState(), $targetEdit.val());
4396 if (globalState[GSTATE__EDIT_FEATURES_DISABLED]()){
4397 // skip
4398 }
4399 else if (evt.keyCode===KEY__ENTER || evt.keyCode === KEY__LEFT_WINDOW || evt.keyCode === KEY__RIGHT_WINDOW || evt.keyCode === KEY__SELECT){
4400 // skip
4401 }
4402 else if ( (evt.keyCode===KEY__DEL || evt.keyCode==KEY__BACKSPACE) && $targetEdit.val().charAt(getTargetEditCursorOffset()) < " ") { // deleting empty line
4403 // skip
4404 }
4405 else{
4406 openAutocompletion();
4407 }
4408 }
4409 });
4410
4411
4412 return {
4413 closePopup,
4414 isPopupOpen,
4415 onTargetSentenceChangedByAlternative
4416 };
4417});
4418
4419
4420// = == ============= End: smartTargetEditing =============== == =
4421
4422
4423
4424
4425const KEY__BACKSPACE = 8;
4426const KEY__TAB = 9;
4427const KEY__ENTER = 13;
4428const KEY__ESC = 27;
4429const KEY__SPACE = 32;
4430const KEY__LEFT = 37;
4431const KEY__UP = 38;
4432const KEY__RIGHT = 39;
4433const KEY__DOWN = 40;
4434const KEY__HOME = 36;
4435const KEY__END = 35;
4436// const KEY__INSERT = 45;
4437const KEY__DEL = 46;
4438const KEY__LEFT_WINDOW = 91;
4439const KEY__RIGHT_WINDOW = 92;
4440const KEY__SELECT = 93;
4441const KEY__PG_UP = 33;
4442const KEY__PG_DOWN = 34;
4443
4444
4445
4446// = == =============== Error handling =============== == =
4447(function _errorHandling(){
4448 const TAG = '[ErrorHandling]';
4449 let lastFailureTime = 0;
4450
4451 on(ON.ON__RPC_CALL_FAILED, function(_requestType, _rpcError){
4452 lastFailureTime = Date.now();
4453 });
4454
4455 on(ON.ON__REQUEST_FAILED, function (requestType, lmtError){
4456
4457 try{
4458 console.log(TAG, "ON.ON__REQUEST_FAILED:", requestType, lmtError);
4459 if (lmtError.errorType === LMT_ERROR_TYPE__RPC_ERROR && lmtError.rpcError && lmtError.statusCode===429){ // overload or blocking
4460 log(TAG, "Overload or blocking");
4461 if (requestType==="autocomplete" || requestType==="alternatives"){
4462 globalState[GSTATE__EDIT_FEATURES_DISABLED](true);
4463 globalState[GSTATE__TYPING_REQUESTS_BLOCKED_UNTIL] = Date.now() * 2; // disable typing requests forever
4464 }else {
4465 globalState[GSTATE__TYPING_REQUESTS_BLOCKED_UNTIL] = Date.now() + conf_blockTypingRequestsTime;
4466
4467 if (lmtError.rpcError.code === 1042901){
4468 var showBlockingWarning = function(ip){
4469 LMT_UI_SystemNotification.addNotification({
4470 id: 'notification_user_blocked',
4471 severity: 'critical',
4472 text: getLocalizedText('translator/notification_user_blocked',
4473 "Due to too many requests from your browser, access to the free DeepL translator has been temporarily disabled. Please try again later.")
4474 + (ip ? " [IP: " + ip +"]" : ""),
4475 priority: 40,
4476 timeout_sec: 60
4477 });
4478 }
4479 RPC.connectTo({
4480 url: "/PHP/backend/account.php?request_type=jsonrpc&il=" + dlPageState.il,
4481 method: 'POST',
4482 withCredentials: true
4483 }).getFunction('getUserIp')().then(function _success(theIp){
4484 showBlockingWarning(theIp);
4485
4486 dlAnalytics.sendOncePerSession({
4487 eventName: 'hasShowed_noticeFreeBlocked',
4488 eventParams: {
4489 category: 'translator',
4490 action: 'showNotice',
4491 label: 'freeBlocked',
4492 },
4493 });
4494 }, function _failure(err){
4495 console.error(err);
4496 showBlockingWarning(false);
4497 });
4498
4499 } else {
4500 LMT_UI_SystemNotification.addNotification({
4501 id: 'notification_overload',
4502 severity: 'critical',
4503 text: getLocalizedText('translator/notification_overload', "Too much load. The functionality of DeepL Translator may be limited.") + " (err429)",
4504 priority: 50,
4505 timeout_sec: 60
4506 });
4507 }
4508 }
4509 } else if(lmtError.errorType === LMT_ERROR_TYPE__RPC_ERROR && lmtError.rpcError && lmtError.statusCode==456){ // quota reached
4510 LMT_UI_SystemNotification.addNotification({
4511 id: 'notification_quotaReached',
4512 severity: 'critical',
4513 text: getLocalizedText('translator/proAccountError.quotaReached', "Quota reached.") +
4514 "<div><a href='/pro-account.html' class='dl_internal_link_row' target='_blank'>" +
4515 getLocalizedText('translator/proAccountError.accountPageButton', "Go to your contract page") +
4516 "</a></div>",
4517 priority: 90,
4518 });
4519 on(ON.ON__RPC_CALL_SUCCESSFULL, function(){
4520 LMT_UI_SystemNotification.removeNotification('notification_quotaReached');
4521 return 'REMOVE';
4522 });
4523 } else if(lmtError.errorType === LMT_ERROR_TYPE__RPC_ERROR && lmtError.rpcError && lmtError.statusCode==403){ // forbidden -> account problems
4524 if (config.CONFIG__IS_APP) {
4525 const containerNode = $('<div class="lmt__system_notification_container"></div>');
4526 const additionalContent = $('<div class="lmt__system_notification_button_container"></div>')
4527 .append( $('<button class="lmt__appButtons__eventActionButton">' + getLocalizedText('translator/proAccountError.accountPageButton', "Go to your contract page") + "</button>")
4528 .click(() => notify(ON.ON__USER_EVENT, {
4529 type: "userRequestsContractPage",
4530 context: "accountProblems"
4531 })))
4532 .append( $('<button class="lmt__appButtons__eventActionButton">' + getLocalizedText('translator/proAccountError.accountLogoutButton', "Logout") + "</button>")
4533 .click(() => notify(ON.ON__USER_EVENT, {
4534 type: "userRequestsLogout",
4535 context: "accountProblems"
4536 })))
4537 .append( $('<button class="lmt__appButtons__eventActionButton">' + getLocalizedText('translator/proAccountError.refreshPageButton', "Refresh") + "</button>")
4538 .click(() => location.reload()));
4539
4540 LMT_UI_SystemNotification.addNotification({
4541 id: 'notification_accountProblem',
4542 severity: 'final',
4543 text: getLocalizedText('translator/proAccountError.accountProblem', "Account problem."),
4544 additionalContent,
4545 priority: 90,
4546 containerNode,
4547 });
4548 setTimeout(
4549 () => {
4550 $('.lmt__appButtons__eventActionButton')[0].focus();
4551 }
4552 );
4553 } else {
4554 const additionalContent = "<div><a href='/pro-account.html' class='dl_internal_link_row' target='_blank'>" +
4555 getLocalizedText('translator/proAccountError.accountPageButton', "Go to your contract page") +
4556 "</a></div>";
4557 LMT_UI_SystemNotification.addNotification({
4558 id: 'notification_accountProblem',
4559 severity: 'critical',
4560 text: getLocalizedText('translator/proAccountError.accountProblem', "Account problem."),
4561 additionalContent,
4562 priority: 90,
4563 });
4564 }
4565
4566 on(ON.ON__RPC_CALL_SUCCESSFULL, function(){
4567 LMT_UI_SystemNotification.removeNotification('notification_accountProblem');
4568 return 'REMOVE';
4569 });
4570
4571 } else if(lmtError.errorType === LMT_ERROR_TYPE__RPC_ERROR){ // connection or server error
4572 globalState[GSTATE__TYPING_REQUESTS_BLOCKED_UNTIL] = Math.max(globalState[GSTATE__TYPING_REQUESTS_BLOCKED_UNTIL], Date.now() + conf_blockTypingRequestsTime);
4573
4574 if (!LMT_UI_SystemNotification.hasNotification('notification_networkProblems')){
4575 LMT_UI_SystemNotification.addNotification({
4576 id: 'notification_networkProblems',
4577 severity: 'critical',
4578 text: getLocalizedText('translator/notification_networkProblems',
4579 "There seems to be a problem with the internet connection. The functionality of DeepL Translator may be limited."),
4580 priority: 100
4581 });
4582 on(ON.ON__RPC_CALL_SUCCESSFULL, function(){
4583 if (Date.now()>lastFailureTime + 20*1000){
4584 LMT_UI_SystemNotification.removeNotification('notification_networkProblems');
4585 return 'REMOVE';
4586 }
4587 });
4588 }
4589 } else { // Internal error
4590 console.error("Unexpected error:", lmtError);
4591 /*LMT_UI_SystemNotification.addNotification({
4592 id: 'notification_internalErrors',
4593 severity: 'critical',
4594 text: getLocalizedText('translator/notification_internalErrors', "An internal error occurred; "),
4595 priority: 100
4596 });*/
4597 }
4598 }catch(e){
4599 console.error(e);
4600 }
4601 });
4602
4603
4604 globalState[GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE].onValueChanged.push(function(value){
4605 if (value){
4606 LMT_UI_SystemNotification.addNotification({
4607 id: 'notification_onlyOneSentence',
4608 severity: 'normal',
4609 text: getLocalizedText('translator/notification_onlyOneSentence',
4610 "<span>!</span> Due to high traffic on the free DeepL translator, the service is limited at the moment. You can only translate one sentence at a time and advanced editing functions are not available."),
4611 priority: 30
4612 });
4613 } else {
4614 LMT_UI_SystemNotification.removeNotification('notification_onlyOneSentence');
4615 }
4616 });
4617 globalState[GSTATE__EDIT_FEATURES_DISABLED].onValueChanged.push(function(value){
4618 if (value){
4619 LMT_UI_SystemNotification.addNotification({
4620 id: 'notification_noAdvancedEditing',
4621 severity: 'normal',
4622 text: getLocalizedText('translator/notification_noAdvancedEditing',
4623 "<span>!</span> Due to high traffic on the free DeepL translator, the service is limited at the moment and advanced editing functions are not available."),
4624 priority: 20
4625 });
4626 } else {
4627 LMT_UI_SystemNotification.removeNotification('notification_noAdvancedEditing');
4628 }
4629 });
4630
4631
4632
4633 on(ON.ON__PARTIAL_TRANSLATION_RECEIVED, function(){
4634 console.warn(ON.ON__PARTIAL_TRANSLATION_RECEIVED);
4635 globalState[GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE](true);
4636 globalState[GSTATE__EDIT_FEATURES_DISABLED](true);
4637 globalState[GSTATE__TYPING_REQUESTS_BLOCKED_UNTIL] = Date.now() * 2; // disable typing requests forever
4638 });
4639 // ??? todo: should a recovery be possible?
4640})();
4641
4642// = == =============== End: Supervise Network =============== == =
4643
4644
4645// = == =============== observeSentenceSelection =============== == =
4646/**
4647 * observe the selection in the target text area.
4648 * If the selection changes onTargetSelectionChanged(tSentencePos_selectionBegin, tSentencePos_selectionEnd) is called.
4649 * - tSentencePos_selectionBegin is null if no target sentences is selected
4650 * - tSentencePos_selectionEnd is set if a range is selected; null otherwise
4651 * \see getSentencePosForTargetCursorOffset(...) for a description of the sentencsPos-Object
4652*/
4653webTranslator.features.define('observeSentenceSelection', ['domElements', 'webTranslatorCore', 'updateTargetSentences'],
4654 function _feature__observeSentenceSelection(domElements, webTranslatorCore, updateTargetSentences){
4655 const TAG = "[observeSentenceSelection]";
4656 const dbg = false;
4657
4658 const onTargetSelectionChanged = U.createMultiProcedure();
4659 const onSourceSelectionChanged = U.createMultiProcedure();
4660 let prevTargetSelectionSummary = "NO_SELECTION";
4661 function _detectSelectedTargetSentences(){
4662 const state = webTranslatorCore.getActiveState();
4663
4664 updateTargetSentences.doRefreshTargetDataFromText(state, domElements.$targetEdit.val());
4665
4666 const offset = getTargetEditCursorOffset();
4667 const tSentencePos_selectionBegin = getSentencePosForTargetCursorOffset(state, offset);
4668 let tSentencePos_selectionEnd;
4669 let selectionSummary;
4670
4671 if (!tSentencePos_selectionBegin){ // no aligned sentence found
4672 dbg && console.log(TAG, "No selection");
4673 selectionSummary = "NO_SELECTION";
4674 } else {
4675 selectionSummary = tSentencePos_selectionBegin.sentenceIndex + "_" + tSentencePos_selectionBegin.sentenceCursorOffset;
4676
4677 const offset2 = domElements.$targetEdit.prop("selectionEnd");
4678 if (offset !== offset2){
4679 tSentencePos_selectionEnd = getSentencePosForTargetCursorOffset(state, offset2);
4680 if (tSentencePos_selectionEnd)
4681 selectionSummary += ":" + tSentencePos_selectionEnd.sentenceIndex + "_" + tSentencePos_selectionEnd.sentenceCursorOffset;
4682 }
4683 }
4684 if (prevTargetSelectionSummary !== selectionSummary){
4685 prevTargetSelectionSummary = selectionSummary;
4686 dbg && console.log(TAG, "selection", selectionSummary);
4687 onTargetSelectionChanged(tSentencePos_selectionBegin || null, tSentencePos_selectionEnd || null);
4688 }
4689 }
4690
4691 let prevSourceSelectionSummary = "NO_SELECTION";
4692 function _detectSelectedSourceSentences(){
4693 const state = webTranslatorCore.getActiveState();
4694
4695 // doRefreshTargetDataFromText(state, domElements.$targetEdit.val());
4696
4697 const offset = getSourceEditCursorOffset();
4698 const sentencePos_selectionBegin = getSentencePosForSourceCursorOffset(state, offset);
4699 let sentencePos_selectionEnd;
4700 let selectionSummary;
4701
4702 if (!sentencePos_selectionBegin){ // no aligned sentence found
4703 dbg && console.log(TAG, "No selection");
4704 selectionSummary = "NO_SELECTION";
4705 } else {
4706 selectionSummary = sentencePos_selectionBegin.sentenceIndex + "_" + sentencePos_selectionBegin.sentenceCursorOffset;
4707
4708 const endOffset = domElements.$sourceEdit.prop("selectionEnd");
4709 if (offset !== endOffset){
4710 sentencePos_selectionEnd = getSentencePosForSourceCursorOffset(state, endOffset);
4711 if (sentencePos_selectionEnd)
4712 selectionSummary += ":" + sentencePos_selectionEnd.sentenceIndex + "_" + sentencePos_selectionEnd.sentenceCursorOffset;
4713 }
4714 }
4715 if (prevSourceSelectionSummary !== selectionSummary){
4716 prevSourceSelectionSummary = selectionSummary;
4717 dbg && console.log(TAG, "selection", selectionSummary);
4718 onSourceSelectionChanged(sentencePos_selectionBegin || null, sentencePos_selectionEnd || null);
4719 }
4720 }
4721
4722
4723 document.addEventListener('selectionchange', () => {
4724 const selection = window.getSelection();
4725 if (selection.focusNode===selection.anchorNode){
4726 if (selection.anchorNode === domElements.targetEdit || selection.anchorNode === domElements.targetEditInnerContainer){
4727 _detectSelectedTargetSentences();
4728 } else if (selection.anchorNode === domElements.sourceEdit || selection.anchorNode === domElements.sourceEditInnerContainer){
4729 _detectSelectedSourceSentences();
4730 }
4731 }
4732 });
4733 // alternative if selectionchange is not supported (e.g. on Firefox)
4734 domElements.$sourceEdit.on('keyup click', () => _detectSelectedSourceSentences());
4735 domElements.$targetEdit.on('keyup click', () => _detectSelectedTargetSentences());
4736
4737 domElements.$sourceEdit.on("blur", () => onTargetSelectionChanged(null, null));
4738 domElements.$targetEdit.on("blur", () => onSourceSelectionChanged(null, null));
4739
4740 return {
4741 onTargetSelectionChanged, // (tSentencePos_selectionBegin, tSentencePos_selectionEnd)
4742 onSourceSelectionChanged // (sSentencePos_selectionBegin, sSentencePos_selectionEnd)
4743 };
4744});
4745// = == =============== end: observeSentenceSelection =============== == =
4746
4747
4748
4749// = == =============== Source sentence highlighting =============== == =
4750config[CONFIG__HIGHLIGHT] && webTranslator.features.define('sourceSentenceHighlight',
4751 ['webTranslatorCore','observeSentenceSelection', 'domElements'],
4752 function _feature__sourceSentenceHighlight(webTranslatorCore, observeSentenceSelection, domElements){
4753 const dbg = false;
4754
4755 const $sourceEdit = domElements.$sourceEdit;
4756 const $targetEdit = domElements.$targetEdit;
4757 const clearSourceHighlight = U.createCallOnce();
4758 const clearTargetHighlight = U.createCallOnce();
4759
4760 let sourceSentenceId_to_nodes = undefined;
4761 let targetSentenceId_to_nodes = undefined;
4762 let $highlightContainer;
4763
4764 on(ON.ON__SOURCE_TEXT_CHANGED, clearSourceHighlight);
4765 $sourceEdit.on("focus click keydown", clearSourceHighlight);
4766
4767 $(window).on("resize", clearSourceHighlight);
4768
4769
4770 observeSentenceSelection.onTargetSelectionChanged.push(_updateHighlightingForCursorPosTarget);
4771 let prevSourceSentence;
4772 function _updateHighlightingForCursorPosTarget(tSentencePos_selectionBegin, tSentencePos_selectionEnd){
4773 clearTargetHighlight();
4774 if (!tSentencePos_selectionBegin || // no sentence selected
4775 (tSentencePos_selectionEnd && tSentencePos_selectionEnd.sentence !== tSentencePos_selectionBegin.sentence)){ // more than one sentence selected
4776 clearSourceHighlight();
4777 return;
4778 }
4779 if (!$highlightContainer){
4780 createHighlight('source', $sourceEdit);
4781 prevSourceSentence = undefined;
4782 }
4783 else {
4784 updateContainerStyling($sourceEdit);
4785 }
4786 { // selecting a sentence may change the sourceEdit size (in the native apps, see ...-feature). Make
4787 // sure that the highglight container is adjusted accordingly.
4788 const t = setTimeout(updateContainerStyling.bind(null, $sourceEdit), 500);
4789 clearSourceHighlight.push(()=>clearTimeout(t));
4790 }
4791
4792 const state = webTranslatorCore.getActiveState();
4793
4794 var targetSentence = tSentencePos_selectionBegin.sentence;
4795 dbg && console.log("[highlight] targetSentence", targetSentence);
4796
4797 var sourceSentence = targetSentence.getSourceSentence();
4798 if (prevSourceSentence!==sourceSentence){
4799 $sourceEdit.parent().find('.lmt__source_textarea_overlay__sentence--active').removeClass('lmt__source_textarea_overlay__sentence--active');
4800 if (sourceSentence && (!globalState[GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE]() || sourceSentence==state[STATE__SOURCE_SENTENCES][0]) ){
4801 // log("[highlight] sourceSentence", sourceSentence);
4802
4803 if (sourceSentenceId_to_nodes === undefined) sourceSentenceId_to_nodes = Object.create(null)
4804 var nodes = sourceSentenceId_to_nodes[sourceSentence[S_SENTENCE__ID]];
4805 nodes && nodes.forEach(function($node){ $node.addClass("lmt__source_textarea_overlay__sentence--active");});
4806 }
4807 prevSourceSentence = sourceSentence;
4808 }
4809 }
4810
4811 // observeSentenceSelection.onSourceSelectionChanged.push(_updateHighlightingForCursorPosSource);
4812 let prevTargetSentence;
4813 function _updateHighlightingForCursorPosSource(sSentencePos_selectionBegin, sSentencePos_selectionEnd){
4814 console.log('HIGHTLIGHT')
4815 clearSourceHighlight();
4816 if (!sSentencePos_selectionBegin || // no sentence selected
4817 (sSentencePos_selectionEnd && sSentencePos_selectionEnd.sentence !== sSentencePos_selectionBegin.sentence)){ // more than one sentence selected
4818 clearSourceHighlight();
4819 return;
4820 }
4821 if (!$highlightContainer){
4822 createHighlight('target', $targetEdit);
4823 prevTargetSentence = undefined;
4824 }
4825 else {
4826 updateContainerStyling($targetEdit);
4827 }
4828 { // selecting a sentence may change the sourceEdit size (in the native apps, see ...-feature). Make
4829 // sure that the highglight container is adjusted accordingly.
4830 const t = setTimeout(updateContainerStyling.bind(null, $targetEdit), 500);
4831 clearSourceHighlight.push(()=>clearTimeout(t));
4832 }
4833
4834 var state = webTranslatorCore.getActiveState();
4835
4836 var targetSentence = sSentencePos_selectionBegin.sentence;
4837 dbg && console.log("[highlight] targetSentence", targetSentence);
4838
4839 var sourceSentence = targetSentence;
4840 if (prevTargetSentence!==sourceSentence){
4841 $targetEdit.parent().find('.lmt__source_textarea_overlay__sentence--active').removeClass('lmt__source_textarea_overlay__sentence--active');
4842 if (sourceSentence && (!globalState[GSTATE__ONLY_TRANSLATE_FIRST_SENTENCE]() || sourceSentence==state[STATE__SOURCE_SENTENCES][0]) ){
4843 // log("[highlight] sourceSentence", sourceSentence);
4844 if (targetSentenceId_to_nodes === undefined) targetSentenceId_to_nodes = Object.create(null)
4845 var nodes = targetSentenceId_to_nodes[sourceSentence[S_SENTENCE__ID]];
4846 nodes && nodes.forEach(function($node){ $node.addClass("lmt__source_textarea_overlay__sentence--active");});
4847 }
4848 prevTargetSentence = sourceSentence;
4849 }
4850 }
4851
4852 function clearSentenceNodes(isSourceEditor) {
4853 if (isSourceEditor) sourceSentenceId_to_nodes = undefined
4854 else targetSentenceId_to_nodes = undefined
4855 }
4856
4857 function createHighlight(type, $editor){
4858 const isSourceEditor = type === 'source';
4859 const clearHighlight = isSourceEditor ? clearSourceHighlight : clearTargetHighlight;
4860
4861 clearHighlight();
4862
4863 if (isSourceEditor) {
4864 sourceSentenceId_to_nodes = new Object(null);
4865 }
4866 else {
4867 targetSentenceId_to_nodes = new Object(null);
4868 }
4869
4870 clearHighlight.push(()=> clearSentenceNodes(isSourceEditor));
4871
4872 $editor.addClass('lmt__textarea--inactive');
4873 clearHighlight.push(()=>$editor.removeClass('lmt__textarea--inactive'));
4874
4875 var $container = $("<div></div>")
4876 .addClass('lmt__source_textarea_overlay')
4877 .addClass('lmt__textarea_base_style')
4878 .insertAfter($editor);
4879 $highlightContainer = $container;
4880 clearHighlight.push(()=>{
4881 $container.remove();
4882 $highlightContainer = null;
4883 });
4884 updateContainerStyling($editor);
4885
4886 const state = webTranslatorCore.getActiveState();
4887 var sentenceEntries = [];
4888 var lineHeight = parseInt($editor.css('line-height'));
4889 state[STATE__SOURCE_SENTENCES].forEach(function(sourceSentence){
4890 if (sourceSentence[S_SENTENCE__WHITESPACES_BEFORE].length){
4891 $("<span></span>")
4892 .text(sourceSentence[S_SENTENCE__WHITESPACES_BEFORE])
4893 // .css("background-color", "#88f")
4894 .appendTo($container);
4895 }
4896 var text = !isSourceEditor ? sourceSentence.translations[TRANSLATION__INITIAL_TRANSLATION_TEXT] : sourceSentence[S_SENTENCE__RAW_TRIMMED_TEXT];
4897 var preNode = $("<span></span>")
4898 .addClass('lmt__source_textarea_overlay__sentence_marker')
4899 .css({ "height": lineHeight + "px" })
4900 .appendTo($container);
4901 var textNode = $("<span></span>")
4902 .addClass('lmt__source_textarea_overlay__text')
4903 .text(text)
4904 .appendTo($container);
4905 var postNode = $("<span></span>")
4906 .addClass('lmt__source_textarea_overlay__sentence_marker')
4907 .css({ "height": lineHeight + "px" })
4908 .appendTo($container);
4909 sentenceEntries.push({
4910 sourceSentence: sourceSentence,
4911 preNode: preNode,
4912 textNode: textNode,
4913 postNode: postNode
4914 });
4915 if (sourceSentence[S_SENTENCE__WHITESPACES_AFTER].length){
4916 $("<span></span>")
4917 .text(sourceSentence[S_SENTENCE__WHITESPACES_AFTER])
4918 //.css("background-color", "#88c")
4919 .appendTo($container);
4920 }
4921 });
4922
4923 var sourceWidth = $editor.outerWidth();
4924 var sourceRight = sourceWidth - parseInt($editor.css('padding-right'));
4925
4926 sentenceEntries.forEach(function(sentenceEntry){
4927 var nodes = [];
4928
4929 if (isSourceEditor) {
4930 sourceSentenceId_to_nodes[sentenceEntry.sourceSentence[S_SENTENCE__ID]] = nodes;
4931 }
4932 else {
4933 targetSentenceId_to_nodes[sentenceEntry.sourceSentence[S_SENTENCE__ID]] = nodes;
4934 }
4935
4936 var textPos = sentenceEntry.textNode.position();
4937 var height = sentenceEntry.textNode.height();
4938
4939 var preNodePos = sentenceEntry.preNode.position();
4940 var postNodePos = sentenceEntry.postNode.position();
4941
4942 nodes.push(sentenceEntry.textNode);
4943 //var theHighlightNodes = [];
4944 if (!dbg){
4945 // disabled
4946 }else if (height<lineHeight*1.5){
4947 nodes.push($("<div></div>").addClass('lmt__source_textarea_overlay__sentence')
4948 .css({
4949 "top": textPos.top + "px",
4950 "left" : preNodePos.left + "px",
4951 "height": lineHeight + "px",
4952 "width": (postNodePos.left-preNodePos.left) + "px",
4953 }).appendTo($container));
4954 }else {
4955 var y = textPos.top;
4956 nodes.push($("<div></div>").addClass('lmt__source_textarea_overlay__sentence')
4957 .css({
4958 "top": y + "px",
4959 "left" : preNodePos.left + "px",
4960 "height": lineHeight + "px",
4961 "width": (sourceRight-preNodePos.left) + "px",
4962 }).appendTo($container));
4963 y += lineHeight;
4964 if (height>lineHeight*2.5){
4965 nodes.push($("<div></div>").addClass('lmt__source_textarea_overlay__sentence')
4966 .css({
4967 "top": y + "px",
4968 "left" : textPos.left + "px",
4969 "height": (height-(2*lineHeight)) + "px",
4970 "width": (sourceRight-textPos.left) + "px",
4971 }).appendTo($container));
4972 y += (height-(2*lineHeight));
4973 }
4974 nodes.push($("<div></div>").addClass('lmt__source_textarea_overlay__sentence')
4975 .css({
4976 "top": y + "px",
4977 "left" : textPos.left + "px",
4978 "height": lineHeight + "px",
4979 "width": (postNodePos.left-textPos.left) + "px",
4980 }).appendTo($container));
4981 }
4982 });
4983 }
4984
4985 function updateContainerStyling($editor){
4986 if ($highlightContainer){
4987 const $sourceEdit = $editor;
4988 const iosTextareaFix = $sourceEdit.hasClass('lmt__textarea--iOSTextareaFix');
4989
4990 $highlightContainer
4991 .css($sourceEdit.dlCSS( "padding-top", "padding-bottom", "padding-left", "padding-right", "margin", "overflow-x", "overflow-y",
4992 "font-family", "font-size", "font-weight", "font-stretch"))
4993 // .css("font",$sourceEdit.css("font"))
4994 .css("line-height", $sourceEdit.css("line-height"))
4995 .height($sourceEdit.height())
4996 .innerWidth($sourceEdit[0].getBoundingClientRect().width - (iosTextareaFix?6:0)); // similar to .width($sourceEdit.width()) but supports floating point sizes;
4997
4998 iosTextareaFix && $highlightContainer.css('padding-left', (parseInt($highlightContainer.css('padding-left')) + 3) + "px" );
4999 //log("iOS FIX", ((parseInt($container.css('padding-left'))+3) + "px" ), $container.css('padding-left'));
5000
5001 }
5002 }
5003
5004 /*$targetEdit.on('change keyup paste keydown', function() {
5005 // your code here
5006 });*/
5007 return {
5008 };
5009}); // ==== end: sourceHighlight
5010
5011
5012// = == =============== Show number of characters in source edit (+ proAd) =============== == =
5013(function (){
5014 let $proAd, adVisible;
5015 let $lengthMarker;
5016 on(ON.ON__SOURCE_TEXT_CHANGED, function _showNumberOfCharacters(){
5017 const len = $sourceEdit.val().length;
5018 if (len>conf_showLengthMarkerMinLength || $lengthMarker){
5019 const domElements = webTranslator.getFeature('domElements');
5020 if (!$lengthMarker){
5021 $lengthMarker = $("<div class='lmt__source_textarea__length_marker'></div>").appendTo(domElements.sourceEditOuterContainer);
5022 }
5023 $lengthMarker.text(isPro ? len : len+ " / " + conf_maxNumCharacters);
5024 if (len>conf_maxNumCharacters){
5025 $(domElements.sourceEditOuterContainer).addClass("lmt__textarea_container--text_too_long");
5026 $lengthMarker.addClass('lmt__source_textarea__length_marker--critical');
5027 }else {
5028 $(domElements.sourceEditOuterContainer).removeClass("lmt__textarea_container--text_too_long");
5029 $lengthMarker.removeClass('lmt__source_textarea__length_marker--critical');
5030 }
5031 }
5032 function initAndGetAd(){
5033 if (!$proAd){
5034 // console.log("AD init");
5035 const domElements = webTranslator.getFeature('domElements');
5036 $proAd = $('.lmt__source_textarea__length_marker_pro_advertisement');
5037 on(ON.ON__SOURCE_TEXT_EMPTY, function(){
5038 $(domElements.sourceEditInnerContainer).removeClass('lmt__textarea_proAd--visible');
5039 $proAd
5040 .removeClass('lmt__source_textarea__length_marker_pro_advertisement--visible')
5041 .detach();
5042 adVisible = false;
5043 });
5044 }
5045 return $proAd;
5046 }
5047 function showAd(){
5048 // console.log("AD show");
5049 const domElements = webTranslator.getFeature('domElements');
5050 adVisible = true;
5051 initAndGetAd()
5052 .css("display","block")
5053 .appendTo($sourceEdit.parent());
5054 setTimeout(function(){
5055 $(domElements.sourceEditInnerContainer).addClass('lmt__textarea_proAd--visible');
5056 $proAd.addClass('lmt__source_textarea__length_marker_pro_advertisement--visible');
5057
5058 dlAnalytics.sendOncePerSession({
5059 eventName: 'hasShowed_noticeFreeLimit',
5060 eventParams: {
5061 category: 'translator',
5062 action: 'showNotice',
5063 label: 'freeLimit',
5064 },
5065 });
5066 }, 100);
5067 }
5068 if (len>conf_proCharacterAdMinLength && !isPro){
5069 adVisible || showAd();
5070 }
5071 });
5072})();
5073// = == =============== end: Show number of chanracters in source edit =============== == =
5074
5075
5076// = == =============== Language selection menu (late) =============== == =
5077// Note: This is partly implemented in translator_early.js
5078webTranslator.features.define('selectLangUI', ['webTranslatorCore', 'domElements', 'eventHandling', 'stateManagement'],
5079 function __feauture__selectLangUI(webTranslatorCore, domElements, eventHandling, stateManagement){
5080
5081 const $rootContainer = domElements.$rootContainer;
5082 const $sourceLangSelect = $rootContainer.find(".lmt__language_select--source");
5083 const $targetLangSelect = $rootContainer.find(".lmt__language_select--target");
5084
5085 // update the text of the sourceLang button
5086 if ($sourceLangSelect.length){
5087 U.withValue([webTranslatorCore.sourceLang, webTranslatorCore._selectedSourceLang, webTranslatorCore.sourceIsEmpty], function _sourceMenu_updateLangName(sourceLang, selectedSourceLang, sourceIsEmpty) {
5088 let text;
5089 if (selectedSourceLang === 'auto') {
5090 if (sourceIsEmpty){
5091 text = getLocalizedText('translator/detectLanguage', "<span class='translate_from'>Translate from </span><strong>any language</strong>");
5092 } else if (sourceLang){
5093 text = getLocalizedText('translator/translateFrom', "<span class='translate_from'>Translate from </span>") +
5094 "<strong>" + $sourceLangSelect.find("[dl-value='" + sourceLang + "']").html() + "</strong> " +
5095 getLocalizedText('translator/translationLanguageDetected', "(detected)");
5096 } else {
5097 text = undefined; // source lang has not been determined while text is present; this is only temporary so don't update the text to prevent flickering
5098 }
5099 } else {
5100 text = getLocalizedText('translator/translateFrom', "<span class='translate_from'>Translate from </span>") +
5101 "<strong>"+$sourceLangSelect.find("[dl-value='" + selectedSourceLang + "']").html().trim()+"</strong>";
5102 }
5103 text && $sourceLangSelect.find('> button > span').html(text);
5104 });
5105 }
5106 if ($targetLangSelect.length || $sourceLangSelect.length){ // Language selection menus
5107 var doCloseLangMenus = U.createCallOnce();
5108 $rootContainer.find(".lmt__language_select > button").click(function _toggleMenuOnClick(_inEvent) {
5109 const $theSelect = $(this).closest(".lmt__language_select");
5110 const wasOpen = $theSelect.hasClass("lmt__language_select--open");
5111 doCloseLangMenus();
5112 if (!wasOpen){
5113 const selectType = $theSelect.is($targetLangSelect) ? 'target' : 'source';
5114 $targetLangSelect.find('[dl-value]').css('display',"");
5115 const selectedSourceLang = webTranslatorCore.getSelectedSourceLang()
5116 if (selectType === 'target'){
5117 let languageToHide;
5118 if (selectedSourceLang !== 'auto'){
5119 languageToHide = selectedSourceLang;
5120 } else if (webTranslatorCore.getActiveState().getSourceLang() && !webTranslatorCore.sourceIsEmpty()){
5121 languageToHide = webTranslatorCore.getActiveState().getSourceLang();
5122 }
5123 languageToHide && $targetLangSelect.find(`[dl-value=${languageToHide}]`).css('display', "none");
5124 }
5125 $theSelect.addClass("lmt__language_select--open");
5126 const t = setTimeout(function(){
5127 $theSelect.addClass("lmt__language_select--open_2");
5128 });
5129 $rootContainer.addClass("lmt--with_open_language_menu");
5130 doCloseLangMenus.push(function(){
5131 clearTimeout(t);
5132 $rootContainer.removeClass("lmt--with_open_language_menu");
5133 $theSelect.removeClass("lmt__language_select--open_2").removeClass('lmt__language_select--open');
5134 });
5135 eventHandling.notify(eventHandling.ON.ON__LANGUAGE_SELECT_OPENED, selectType, $theSelect.get(0));
5136 }
5137 });
5138 eventHandling.on(eventHandling.ON.ON__ANYCLICK, function($targetElement, _evt){
5139 $targetElement.closest(".lmt__language_select").length || doCloseLangMenus();
5140 });
5141 $sourceLangSelect.find(".lmt__language_select__menu button").click(function() {
5142 var langCode = this.getAttribute('dl-value');
5143 stateManagement.onUserSelectsSourceLang(langCode)
5144 doCloseLangMenus();
5145 });
5146 $targetLangSelect.find(".lmt__language_select__menu button").click(function() {
5147 const langCode = this.getAttribute('dl-value')
5148 stateManagement.onUserSelectsTargetLang(langCode)
5149 doCloseLangMenus();
5150 });
5151
5152 {
5153 // for some reason DOM on target side is different, so we need to adjust:
5154 let $oldTranslateToTitle = $targetLangSelect.find('.lmt__language_select__active__title') // reference to old element
5155 let $newTranslateToTitle = $oldTranslateToTitle.clone() // container for new element
5156 let $newTranslateToTitleTail = $newTranslateToTitle.find('strong').clone() // copy 'lang' for later
5157 $newTranslateToTitle.find('strong').remove() // remove 'lang' so we have beginning of the text left only
5158 let newTranslateToTitleHead = $newTranslateToTitle.text() // save text
5159 $newTranslateToTitle.empty() // empty so we can rebuild the way we need
5160 $newTranslateToTitle.append('<span class="translate_to">'+newTranslateToTitleHead+'</span>') // wrap beginning of the text in new structure
5161 $newTranslateToTitle.append($newTranslateToTitleTail) // add 'lang' back
5162 $oldTranslateToTitle.replaceWith($newTranslateToTitle) // replace old element with new one
5163
5164 let translateFromWidth = $sourceLangSelect.find('.translate_from').width()
5165 let translateToWidth = $targetLangSelect.find('.translate_to').width()
5166 let paddingOffset = 0
5167
5168 $sourceLangSelect.find('.lmt__language_select__menu').css({'left': translateFromWidth-paddingOffset})
5169 $targetLangSelect.find('.lmt__language_select__menu').css({'left': translateToWidth-paddingOffset})
5170 }
5171 }
5172 return true;
5173});
5174
5175// = == =============== end: Language selection menu (late) =============== == =
5176
5177
5178// = == =============== Textarea UI =============== == =
5179(function(){
5180 function getTranslatorSide(el) {
5181 if ($(el).hasClass('lmt__source_textarea')) {
5182 return 'source';
5183 }
5184 else if ($(el).hasClass('lmt__target_textarea')) {
5185 return 'target';
5186 }
5187 }
5188
5189 $rootContainer.find(".lmt__textarea").on("focus", function() {
5190 $(this).closest(".lmt__textarea_container").addClass("focus lmt__textarea_container--focus");
5191
5192 let translatorSide = getTranslatorSide(this);
5193 if (translatorSide) {
5194 $(this).closest(".lmt__sides_container").addClass("lmt__sides_container--focus_" + translatorSide);
5195 }
5196 });
5197 $rootContainer.find(".lmt__textarea").on("blur", function() {
5198 $(this).closest(".lmt__textarea_container").removeClass("focus lmt__textarea_container--focus");
5199 $(this).closest(".lmt__sides_container").removeClass("lmt__sides_container--focus_source");
5200 $(this).closest(".lmt__sides_container").removeClass("lmt__sides_container--focus_target");
5201 });
5202 // fire an initial focus-event as autofocus-attribute doesn't fire it itself
5203 $rootContainer.find(".lmt__textarea[autofocus]").first().focus()
5204
5205 if($sourceEdit.has(":focus")) {
5206 $sourceEdit.closest(".lmt__textarea_container").addClass("focus lmt__textarea_container--focus");
5207 }
5208
5209 if (!DeviceProps.is_touch_device) {
5210 $rootContainer.find(".lmt__textarea_placeholder_text")
5211 .hover(function() { $(this).parent().find("textarea").addClass("hover")},
5212 function() { $(this).parent().find("textarea").removeClass("hover") });
5213 }
5214 $rootContainer.find(".lmt__textarea_placeholder_text")
5215 .click(function() { $(this).parent().find("textarea").focus() });
5216})();
5217// = == =============== end: Textarea UI =============== == =
5218
5219
5220
5221// = == =============== Clipboard alterations =============== == =
5222
5223/** Listen on 'paste'-events inside the source / target area
5224 --> [ON.ON__INTERCEPT_PASTED_TEXT] ($textarea, ValueWrapper inOutTextToInsert, ClipboardData, OriginalPasteEvent)
5225
5226 Listen on 'copy'-events inside the target area
5227 --> [ON.ON__INTERCEPT_COPIED_TRANSLATION] (ValueWrapper inOutTextToCopy)
5228*/
5229(function(){
5230 function insertTextIntoActiveTextarea(text){
5231 let insertSuccessfull;
5232 try{
5233 insertSuccessfull = document.execCommand('insertText', false, text);
5234 }catch(e){
5235 console.warn('[insertTextIntoActiveTextarea]', e);
5236 }
5237 if (!insertSuccessfull){
5238 var editNode = document.activeElement;
5239 log("[insertTextIntoActiveTextarea]", editNode);
5240 if (!editNode || editNode.tagName.toLowerCase()!='textarea'){
5241 console.warn("[insertTextIntoActiveTextarea]", editNode);
5242 return false;
5243 }
5244 var startOffset = editNode.selectionStart;
5245 var endOffset = editNode.selectionEnd;
5246 var originalText = $(editNode).val();
5247 $(editNode).val(originalText.substr(0, startOffset) + text + originalText.substr(endOffset));
5248 editNode.setSelectionRange(startOffset + text.length, startOffset + text.length);
5249 }
5250 return true;
5251 }
5252
5253 isPro && on(ON.ON__INTERCEPT_PASTED_TEXT, function($textarea, textToInsert, _clipboardData, _e){
5254 var t = textToInsert();
5255 if ($textarea.is('.lmt__source_textarea') && t.length>conf_interceptPastedTextMinLength){
5256 var editNode = document.activeElement;
5257 var startOffset = editNode.selectionStart;
5258 var endOffset = editNode.selectionEnd;
5259
5260
5261 var $askPastePopup = $('.lmt__ask_paste_popup');
5262 var $text = $askPastePopup.find("> div > p");
5263 var $cancelButton = $askPastePopup.find('.lmt__ask_paste_popup__cancel_button');
5264 var $continueButton = $askPastePopup.find('.lmt__ask_paste_popup__continue_button');
5265 var template = $text.attr('dl-text-template');
5266
5267 var $inputElements = $('.lmt__source_textarea, .lmt__clear_text_button, .lmt__target_textarea');
5268 $inputElements.attr('disabled', 'true');
5269
5270 var close = function _close(){
5271 $inputElements.removeAttr('disabled');
5272 $askPastePopup.removeClass('lmt__ask_paste_popup--visible_2');
5273 $askPastePopup.removeClass('lmt__ask_paste_popup--visible');
5274 $text.text(" ");
5275 $cancelButton.off('click');
5276 $continueButton.off('click');
5277
5278 }
5279 $cancelButton.on('click', close);
5280 $continueButton.on('click', function(){
5281 setTimeout(function(){
5282 $('.lmt__source_textarea')[0].select();
5283 $('.lmt__source_textarea')[0].setSelectionRange(startOffset, endOffset);
5284 insertTextIntoActiveTextarea(t);
5285 checkForChangedSourceEdit();
5286 });
5287 close();
5288 });
5289 editNode.blur();
5290 $cancelButton.select();
5291
5292 $text.text(template.replace("%%num%%", t.length));
5293 $askPastePopup.addClass('lmt__ask_paste_popup--visible');
5294 setTimeout(function(){
5295 $askPastePopup.addClass('lmt__ask_paste_popup--visible_2');
5296 },100);
5297
5298 textToInsert(false);
5299
5300 }
5301 });
5302 [$sourceEdit, $targetEdit].forEach(function($textarea){
5303 $textarea.on('paste', function(evt){
5304 log("[paste]..");
5305 var e = evt.originalEvent;
5306 var clipboardData = e.clipboardData || window.clipboardData || "";
5307 var originalText = clipboardData.getData("Text").trim().replace(/\r/g,""); // in Windows, the text may contain \r characters
5308 var textToInsert = U.value(originalText);
5309 notify(ON.ON__INTERCEPT_PASTED_TEXT, $textarea, textToInsert, clipboardData, e);
5310 if (textToInsert()===false){
5311 e.preventDefault();
5312 }else if (originalText!==textToInsert()){
5313 log("[paste] Text has been altered: ", textToInsert());
5314 if (insertTextIntoActiveTextarea(textToInsert())){
5315 return preventPropagationForEvent(evt);
5316 }
5317 console.warn("[paste] Could not insert altered text."); // ... continue with normal paste event
5318 }
5319 });
5320 });
5321 // conf_interceptPastedTextMinLength
5322
5323 $targetEdit.on('copy', function(evt){
5324 var e = evt.originalEvent;
5325 var selectionObj = window.getSelection();
5326 var selectedText = "" + selectionObj;
5327
5328 // firefox-bug: empty selection in textarea (https://bugzilla.mozilla.org/show_bug.cgi?id=85686)
5329 if (selectedText.length===0){
5330 var startOffset = $targetEdit[0].selectionStart;
5331 var endOffset = $targetEdit[0].selectionEnd;
5332 selectedText = $targetEdit.val().substr(startOffset, endOffset-startOffset);
5333 }
5334 var textToCopy = U.value(selectedText);
5335 notify(ON.ON__INTERCEPT_COPIED_TRANSLATION, textToCopy);
5336 if (selectedText!==textToCopy()){
5337 if (e && e.clipboardData && e.clipboardData.setData){
5338 e.clipboardData.setData('text/plain', textToCopy());
5339 e.preventDefault();
5340 }else{
5341 log("Clipboard could not be updated.");
5342 }
5343 }
5344 });
5345})();
5346
5347// Fix for multi-line text with "<br>" copied from the deepl-homepage -> remove linebreaks before pasting.
5348on(ON.ON__INTERCEPT_PASTED_TEXT, function($edit, textToInsert, clipboardData, _e){
5349 var types = (clipboardData && clipboardData.types) || "";
5350 if (((types instanceof DOMStringList) && types.contains("text/html")) || (types.indexOf && types.indexOf('text/html') !== -1)) {
5351 var html = clipboardData.getData("text/html");
5352 if (html && (html.indexOf('<br class="wide"')!=-1 || html.indexOf('<br class="narrow"')!=-1)){
5353 textToInsert(textToInsert().replace(/(\S) *\n *(\S)/g,"$1 $2"));
5354 log("Fix special line breaks");
5355 }
5356 }
5357});
5358
5359var COPY_FOOTERS = {
5360 'DE': "Übersetzt mit www.DeepL.com/Translator (kostenlose Version)",
5361 'EN': "Translated with www.DeepL.com/Translator (free version)",
5362 'ES': "Traducción realizada con la versión gratuita del traductor www.DeepL.com/Translator",
5363 'FR': "Traduit avec www.DeepL.com/Translator (version gratuite)",
5364 'IT': "Tradotto con www.DeepL.com/Translator (versione gratuita)",
5365 'NL': "Vertaald met www.DeepL.com/Translator (gratis versie)",
5366 'PL': "Przetłumaczono z www.DeepL.com/Translator (wersja darmowa)",
5367 'RU': "Переведено Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ www.DeepL.com/Translator (беÑÐ¿Ð»Ð°Ñ‚Ð½Ð°Ñ Ð²ÐµÑ€ÑиÑ)",
5368 'PT': "Traduzido com a versão gratuita do tradutor - www.DeepL.com/Translator"
5369};
5370
5371// Add footer to translations
5372config[CONFIG__ADD_TRANSLATION_FOOTER] && (function(){
5373 // Text: translator/translationFooter
5374 var footers = COPY_FOOTERS;
5375
5376 var MIN_LENGTH = 400;
5377 var EVENT_COUNT = 3;
5378
5379 function getFooterEvents(){
5380 var events = LMT_Utils.PersistStorage.getValue("LMT_copyEvents");
5381 return Array.isArray(events) ? events: [];
5382 }
5383 function registerFooterEvent(){
5384 var events = getFooterEvents();
5385 events.push(Date.now());
5386 events.splice(0, events.length-EVENT_COUNT);
5387 LMT_Utils.PersistStorage.setValue("LMT_copyEvents", events);
5388 }
5389 function countRecentFooterEvents(){
5390 let count = 0;
5391 var recent_t = Date.now() - 60*60*24*1000;
5392 getFooterEvents().forEach(function(t){
5393 t > recent_t && ++count;
5394 });
5395 return count;
5396 }
5397
5398
5399 on(ON.ON__INTERCEPT_COPIED_TRANSLATION, function(textToCopyWrapper){
5400 var text = textToCopyWrapper();
5401 if (text.length<MIN_LENGTH)
5402 return;
5403 for (let lang in footers){
5404 if (text.search(footers[lang])>=0)
5405 return;
5406 }
5407 log("[TranslationFooter] Events: ", countRecentFooterEvents(), "length", text.length);
5408 if (countRecentFooterEvents()>=EVENT_COUNT){
5409 log("[TranslationFooter] Don't add footer.");
5410 } else {
5411 log("[TranslationFooter] Add footer.");
5412 textToCopyWrapper(text + "\n\n" + (footers[webTranslator.getActiveState().getTargetLang()] || footers['EN']));
5413 }
5414 registerFooterEvent();
5415 });
5416 on(ON.ON__INTERCEPT_PASTED_TEXT, function($edit, textToInsertWrapper, _clipboardData, _e){
5417 var text = textToInsertWrapper();
5418 if (text){
5419 for (let lang in footers){
5420 var footer = footers[lang];
5421 if (text.endsWith(footer) && text.length>=MIN_LENGTH + footer.length){
5422 textToInsertWrapper(text.substr(0, text.length-footer.length));
5423 break;
5424 }
5425 }
5426 }
5427 });
5428})();
5429
5430// = == =============== end: Clipboard alterations =============== == =
5431
5432
5433// = == =============== Batched translation popup =============== == =
5434on(ON.ON__START_BATCHED_TRANSLATION_REQUEST, function(batchController, rqContext){
5435 const dbg = false;
5436 if (batchController.numNotCachedBatchesToTranslate()>1){
5437 dbg && console.log("<<<<<<<");
5438 rqContext.onRequestEnded.push(function(){
5439 dbg && console.log(">>>>>>>>>>");
5440 });
5441
5442 batchController.currentBatchId.onValueChanged.push(function(batchId){
5443 if (batchId===1){ // batchId 0 may be reached by requests that are canceled immediately due to language updates
5444 var $progressPopup = $('.lmt__progress_popup');
5445 var $text = $progressPopup.find("> div > p");
5446 var $progress = $progressPopup.find('.lmt__progress_popup__progress div');
5447 var $stopButton = $progressPopup.find('.lmt__progress_popup__stop_button');
5448 var template = $text.attr('dl-text-template');
5449 var total = batchController.numCharactersToTranslate() || 1;
5450
5451 var $inputElements = $('.lmt__source_textarea, .lmt__clear_text_button, .lmt__target_textarea');
5452 $stopButton.on('click', function(){
5453 batchController.stopped(true);
5454 $progressPopup.addClass('lmt__progress_popup--stopping');
5455 });
5456
5457 $inputElements.attr('disabled', 'true');
5458 var update = function _update(){
5459 var remainingCharacters = batchController.numRemainingCharacters();
5460 dbg && console.log("------- : ", remainingCharacters);
5461 $text.text(template.replace("%%num%%", total-remainingCharacters).replace("%%total%%",total));
5462 $progress.css('width', (100.0 - (remainingCharacters / total) * 100) + "%");
5463 }
5464 update();
5465 $progressPopup.addClass('lmt__progress_popup--visible');
5466 setTimeout(function(){
5467 $progressPopup.addClass('lmt__progress_popup--visible_2');
5468 },100);
5469
5470 var closePopup = function _closePopup(){
5471 $inputElements.removeAttr('disabled');
5472 $progressPopup.removeClass('lmt__progress_popup--visible_2');
5473 $progressPopup.removeClass('lmt__progress_popup--visible');
5474 $progressPopup.removeClass('lmt__progress_popup--stopping');
5475 $progress.css('width', "0%");
5476 $text.text(" ");
5477 $stopButton.off('click');
5478 }
5479 batchController.finished.onValueChanged.push(function(v){
5480 v && closePopup();
5481 });
5482
5483 batchController.numRemainingCharacters.onValueChanged.push(update);
5484 rqContext.onRequestEnded.push(closePopup);
5485 }
5486 });
5487 }
5488});
5489// = == =============== validate Pro access =============== == =
5490
5491isPro && webTranslator.features.define('confirmProAccount', ['domElements'], function _Feature__confirmProAccount(domElements){
5492
5493 function onIsConfirmed(){
5494 $('html').addClass('dl_pro--confirmed');
5495 if(window.location.href.indexOf('beta.deepl.com') === -1) {
5496 H2.connectNodesToValues(domElements.rootContainer, {
5497 usingProHint: {
5498 type: "lmt_usingProBanner"
5499 }
5500 })
5501 }
5502 }
5503
5504 if (dlPageState.loggedIn){
5505 (dlRPC.getBackend_account().callFunction('getActiveSubscriptionInfo')).then(function(subscriptionInfo){
5506 if (subscriptionInfo['isActive'] && subscriptionInfo['supportsWebTranslator'] ){
5507 onIsConfirmed();
5508 }else{
5509 $('html').addClass('dl_pro--invalid');
5510 $('.lmt__source_textarea, .lmt__clear_text_button, .lmt__target_textarea').attr("disabled","true");
5511 if (subscriptionInfo['isActive']){ // !supportsWebTranslator
5512 LMT_UI_SystemNotification.addNotification({
5513 id: 'proAccountError.noSubscription', // accountPageButton
5514 severity: 'final',
5515 text: getLocalizedText('translator/proAccountError.noAccess', "No Access.") +
5516 //"<div><a href='/pro-account.html' class='dl_internal_link_row'>" +
5517 //getLocalizedText('translator/proAccountError.accountPageButton', "Go to your contract page") +
5518 // "</a></div>" +
5519 "<div><a href='#' class='dl_internal_link_row' onclick='kDeepL.logout(undefined,\"/translator\")'>" +
5520 getLocalizedText('translator/proAccountError.noAccess_logoutButton', "Log out") +
5521 "</a></div>",
5522 priority: 100,
5523 });
5524 } else {
5525 if (config.CONFIG__IS_APP) {
5526 const containerNode = $('<div class="lmt__system_notification_container"></div>');
5527 const additionalContent = $('<div class="lmt__system_notification_button_container"></div>')
5528 .append( $('<button class="lmt__appButtons__eventActionButton">' + getLocalizedText('translator/proAccountError.accountPageButton', "Go to your contract page") + "</button>")
5529 .click(() => notify(ON.ON__USER_EVENT, {
5530 type: "userRequestsContractPage",
5531 context: "accountProblems"
5532 })))
5533 .append( $('<button class="lmt__appButtons__eventActionButton">' + getLocalizedText('translator/proAccountError.logoutAndUseFreeButton', "Logout") + "</button>")
5534 .click(() => notify(ON.ON__USER_EVENT, {
5535 type: "userRequestsLogout",
5536 context: "accountProblems"
5537 })))
5538 .append( $('<button class="lmt__appButtons__eventActionButton">' + getLocalizedText('translator/proAccountError.refreshPageButton', "Refresh") + "</button>")
5539 .click(() => location.reload()));
5540
5541 LMT_UI_SystemNotification.addNotification({
5542 id: 'proAccountError.noSubscription',
5543 severity: 'final',
5544 text: getLocalizedText('translator/proAccountError.noSubscription', "Account problem."),
5545 additionalContent,
5546 priority: 100,
5547 containerNode,
5548 });
5549 setTimeout(
5550 () => {
5551 $('.lmt__appButtons__eventActionButton')[0].focus();
5552 }
5553 );
5554 }
5555 else {
5556 const additionalContent = "<div><a href='/pro-account.html' class='dl_internal_link_row'>" +
5557 getLocalizedText('translator/proAccountError.accountPageButton', "Go to your contract page") +
5558 "</a></div>" +
5559 "<div><a href='#' class='dl_internal_link_row' onclick='kDeepL.logout(undefined,\"/translator\")'>" +
5560 getLocalizedText('translator/proAccountError.logoutAndUseFreeButton', "Log out") +
5561 "</a></div>";
5562 LMT_UI_SystemNotification.addNotification({
5563 id: 'proAccountError.noSubscription',
5564 severity: 'final',
5565 text: getLocalizedText('translator/proAccountError.noSubscription', "Account problem."),
5566 additionalContent,
5567 priority: 100,
5568 });
5569 }
5570 }
5571 }
5572 });
5573 } else if(dlPageState.ep){
5574 M.requireAsync(['dlEnterpriseAccount'], function(dlEnterpriseAccount){
5575 if (dlEnterpriseAccount.isConfirmed()) {
5576 onIsConfirmed();
5577 } else {
5578 dlEnterpriseAccount.isConfirmed.onValueChanged.push(function(isValid){
5579 if (isValid){
5580 onIsConfirmed();
5581 return 'REMOVE';
5582 }});
5583 }
5584 });
5585 }
5586});
5587
5588
5589// = == =============== end: validate Pro access =============== == =
5590
5591// End: Core (Note: try to add new functions as features if possible)
5592// ============================================================================================================================================================
5593
5594
5595// ============================================================================================================================================================
5596// == Features
5597// Note: Prefer to add new functions as feature instead of adding them to the core if possible.
5598
5599
5600webTranslator.features.define('webTranslatorCore', [], function __feauture__webTranslatorCore(){
5601 return webTranslator;
5602});
5603
5604
5605// = == =============== Adjust textarea size =============== == =
5606
5607webTranslator.features.define('adjustTextareaSize', ['domElements', 'eventHandling', 'config'],
5608 function __feauture__selectLangUI(domElements, eventHandling, config){
5609
5610 const scrollMode = config['scrollMode'] || 'web';
5611
5612 // update text area layout after resize
5613 window.addEventListener('resize', _evt => recalculateFontSizeAndScrollHeight(true)); // why true?!?
5614
5615 // update text area layout after text has been inserted ranging over the border of the text field
5616 [domElements.sourceEdit, domElements.targetEdit].forEach(textarea => textarea.addEventListener('scroll',
5617 _evt => {
5618 textarea.scrollTop = 0;
5619 _evt.preventDefault();
5620 U.setTimeout_consolidated("updateFontAfterTextareascroll", function(){recalculateFontSizeAndScrollHeight(false);}, 100);
5621 return false;
5622 },
5623 {passive: false}
5624 ));
5625
5626 /**
5627 * Recalulate font sizes when the source text changed.
5628 *
5629 * @listens LMT_Event ON.ON__SOURCE_TEXT_CHANGED
5630 */
5631 eventHandling.on(eventHandling.ON.ON__SOURCE_TEXT_CHANGED, _rqContext => recalculateFontSizeAndScrollHeight(true) );
5632
5633 /**
5634 * Recalulate font sizes when target text changed after updating the translation.
5635 *
5636 * @listens LMT_Event ON.ON__TRANSLATION_READY
5637 */
5638 eventHandling.on(eventHandling.ON.ON__TRANSLATION_READY, _info => recalculateFontSizeAndScrollHeight(false));
5639
5640 eventHandling.on(eventHandling.ON.ON__TARGET_EDIT_TEXT_CHANGED, reason =>{
5641 if (reason === 'initial' || reason === 'update')
5642 recalculateFontSizeAndScrollHeight(false);
5643 });
5644
5645 let dictContainerHeight = U.value(0);
5646 U.listenOn('LMT/dictDisplayed', function({ dictHeight }) {
5647 dbg && console.log(TAG, 'LMT/dictDisplayed', 'dictHeight:', dictHeight)
5648 // dictContainerHeight(dictHeight > 700 ? (dictHeight*0.85) : (dictHeight*0.5));
5649 dictContainerHeight(dictHeight);
5650 recalculateFontSizeAndScrollHeight(false);
5651 })
5652 U.listenOn('LMT/dictRemoved', function() {
5653 dbg && console.log(TAG, 'LMT/dictRemoved');
5654 dictContainerHeight(0);
5655 recalculateFontSizeAndScrollHeight(false);
5656 })
5657
5658 domElements.targetEdit.addEventListener('focus', function(_event) { // when gaining focus, the initial alternatives can disappear...
5659 recalculateFontSizeAndScrollHeight(false);
5660 });
5661 setTimeout(()=>recalculateFontSizeAndScrollHeight(true)); // init
5662
5663 const fontScalingClass = U.value(undefined);
5664 {
5665 const removeScalingClass = U.createCallOnce();
5666 U.withValue(fontScalingClass, fontScalingClass => {
5667 removeScalingClass();
5668 //fontScalingClass && [domElements.sourceEditOuterContainer, domElements.targetEditOuterContainer].forEach(node => {
5669 // node.classList.add(fontScalingClass);
5670 // removeScalingClass.push(() => node.classList.remove(fontScalingClass));
5671 //});
5672 if (fontScalingClass){
5673 domElements.rootContainer.classList.add(fontScalingClass);
5674 removeScalingClass.push(() => domElements.rootContainer.classList.remove(fontScalingClass));
5675 }
5676 // console.log(TAG, "fontScalingClass", fontScalingClass);
5677 });
5678 }
5679
5680
5681 const tmp_cleanup = U.createCallOnce();
5682
5683 // let lastHeight = 0;
5684 // let lastHeightAlternatives = 0;
5685 let _lastSourceHeight = 0;
5686 function recalculateFontSizeAndScrollHeight(inSourceTextChanged) {
5687 const TAG = "[_recalculateFontSizeAndScrollHeight] ";
5688 const dbg = queryVars["debugSizes"] || false;
5689
5690 dbg && console.log(TAG, "inSourceTextChanged=",inSourceTextChanged);
5691
5692 const $sourceEdit = domElements.$sourceEdit;
5693 const $targetEdit = domElements.$targetEdit;
5694 if ($sourceEdit.scrollTop()!=0) {
5695 $sourceEdit.scrollTop(0);
5696 setTimeout(function() { recalculateFontSizeAndScrollHeight(inSourceTextChanged); });
5697 return;
5698 }
5699
5700 const theSourceText = $sourceEdit.val();
5701 if (inSourceTextChanged ) { // update font scaling
5702 if (theSourceText.length>1000){ // long text -> skip line counting for speedup
5703 fontScalingClass('lmt--very_long_text');
5704 } else {
5705 const sourceEditStyle = window.getComputedStyle(domElements.sourceEdit);
5706 const sourceTextWidth = domElements.sourceEdit.clientWidth
5707 - (parseInt(sourceEditStyle.paddingLeft)||0)
5708 - (parseInt(sourceEditStyle.paddingRight)||0);
5709
5710 let approxLineCount = 0;
5711 // Approximate number of lines for paragraphs (10.0 ~= lines * pixel / character)
5712 theSourceText.split("\n").forEach( paragraph => approxLineCount += Math.ceil(paragraph.length * 10.0 / sourceTextWidth + 0.1));
5713 dbg && console.log(TAG, "sourceTextWidth", sourceTextWidth, "approxLineCount", approxLineCount);
5714
5715 if (approxLineCount <= 1) {
5716 fontScalingClass('lmt--short_text');
5717 } else if (approxLineCount <= 3) {
5718 fontScalingClass('lmt--medium_text');
5719 } else if (approxLineCount <= (DeviceProps.isMobilePhone?5:15)) {
5720 fontScalingClass('lmt--long_text');
5721 } else {
5722 fontScalingClass('lmt--very_long_text');
5723 }
5724 dbg && console.log(TAG, "numLines=" + approxLineCount + ", fontScalingClass=" + fontScalingClass());
5725 }
5726 }
5727 tmp_cleanup();
5728 const $theScrollarea = $sourceEdit.closest(".lmt__inner_textarea_container");
5729 const theTargetText = $targetEdit.val();
5730
5731 // calculate the actual size of the source and target text.
5732 const $theTextareaDummy = $("<div class='lmt__textarea' style='min-height: 0;'></div>")
5733 .css($sourceEdit.dlCSS("font", "font-size", "font-family", "font-weight","font-variant-ligatures", "padding-top", "padding-bottom", "padding-left", "padding-right"))
5734 .css({
5735 "white-space": "pre-wrap",
5736 "font-variant-ligatures": "none",
5737 "transform": "translate(0%, 100%)", // for debugging
5738 })
5739 .text(theTargetText + ":")
5740 .appendTo($theScrollarea);
5741
5742 dbg && console.log(TAG, "theScrollarea", $theScrollarea)
5743 dbg && console.log(TAG, "theTextarea.height", $theTextareaDummy.height())
5744
5745 let theTargetHeight = $theTextareaDummy.height(); // + 30; // the additional offset is for the toolbar
5746 let theSourceHeight;
5747 if (inSourceTextChanged) {
5748 $theTextareaDummy.text(theSourceText + ":");
5749 theSourceHeight = $theTextareaDummy.height();
5750 const theSourceMinHeight = parseInt($sourceEdit.css("min-height")) - parseInt($sourceEdit.css("padding-top")) - parseInt($sourceEdit.css("padding-bottom"));
5751 if (theSourceMinHeight>theSourceHeight)
5752 theSourceHeight = theSourceMinHeight;
5753 _lastSourceHeight = theSourceHeight;
5754 }
5755 else {
5756 //theSourceHeight = $sourceEdit.height();
5757 theSourceHeight = _lastSourceHeight;
5758 }
5759 tmp_cleanup.push( _ => $theTextareaDummy.remove());
5760 tmp_cleanup();
5761
5762 // Set the heights of the text fields accordingly.
5763 dbg && console.log(TAG, "Needed textarea heights: source_height=" + theSourceHeight + ", target_height=" + theTargetHeight);
5764 if (theSourceHeight<30) theSourceHeight = 30;
5765 if (theTargetHeight<30) theTargetHeight = 30;
5766
5767 if (scrollMode === 'web'){
5768 if (DeviceProps.isMobileLayout) {
5769 $sourceEdit.height(theSourceHeight);
5770 $targetEdit.height(theTargetHeight);
5771
5772 notify(ON.ON__TEXT_AREAS_RESTYLED);
5773 } else {
5774 let theTargetAlternativesHeight = domElements.$translationsAsText.height();
5775 const theMaxHeight = theSourceHeight > theTargetHeight + theTargetAlternativesHeight ?
5776 theSourceHeight :
5777 theTargetHeight + theTargetAlternativesHeight;
5778 $sourceEdit.height(theMaxHeight);
5779 $targetEdit.height(theMaxHeight);
5780
5781 // main resize
5782 //console.log('recalculating with dictHeight', theMaxHeight, dictContainerHeight())
5783 // isPro && $rootContainer.height(theMaxHeight + 640 + dictContainerHeight());
5784 // isPro && $rootContainer.height(theMaxHeight + dictContainerHeight() + (dictContainerHeight() > 0 ? 400 : 640));
5785
5786 notify(ON.ON__TEXT_AREAS_RESTYLED);
5787 /* if (inSourceTextChanged || theMaxHeight >= lastHeight || theTargetAlternativesHeight >= lastHeightAlternatives) {
5788 dbg && console.log(TAG, "HEIGHTS: source="+theSourceHeight+", target="+theTargetHeight+", target_alternatives="+theTargetAlternativesHeight+", new_source="+theMaxHeight+", new_target="+(theMaxHeight-theTargetAlternativesHeight)+", inSourceTextChanged="+inSourceTextChanged);
5789 theSourceHeight = theMaxHeight;
5790 theTargetHeight = theMaxHeight - theTargetAlternativesHeight;
5791 lastHeight = theMaxHeight;
5792 lastHeightAlternatives = theTargetAlternativesHeight;
5793
5794 dbg && console.log(TAG, "Setting textarea heights: source_height="+theSourceHeight+", target_height="+theTargetHeight)
5795 $sourceEdit.height(theSourceHeight);
5796 $targetEdit.height(theTargetHeight);
5797
5798 notify(ON.ON__TEXT_AREAS_RESTYLED);
5799 }*/
5800 }
5801 } else { // 'app'
5802 $sourceEdit.height(theSourceHeight);
5803 $targetEdit.height(theTargetHeight);
5804
5805 notify(ON.ON__TEXT_AREAS_RESTYLED);
5806 }
5807 }
5808});
5809
5810// = == =============== end: Adjust textarea size =============== == =
5811
5812
5813// = == =============== User Settings Feature =============== == =
5814/*
5815 Persistent storage for user settings and data based on local storage.
5816 - the storage is bound to the webTranslator-id (different storages for different types of translators)
5817 - by default, entries expire after a month
5818*/
5819webTranslator.features.define('userSettings', ['webTranslatorCore'], function __feature__userSettings(webTranslatorCore){
5820 const storage = new Map;
5821 return {
5822 getEntry: function(key, _defaultValue, _timeout){
5823 storage.has(key) || storage.set(key, U.persistentValue(webTranslatorCore.id + "." + key, _defaultValue, _timeout === undefined ? 60*60*24*30 : _timeout));
5824 return storage.get(key);
5825 }
5826 };
5827});
5828// = == =============== end: User Settings Feature =============== == =
5829
5830
5831// = == =============== Share feature =============== == =
5832webTranslator.features.define('share', ['webTranslatorCore', 'stateManagement', 'domElements'], (webTranslatorCore, stateManagement, domElements) => {
5833 const TAG = "[share]";
5834 const dbg = false;
5835 const MAX_TEXT_LENGTH = 1900;
5836
5837 function updateCanonical(){
5838 log(TAG, "update canonical");
5839 if (DeviceProps.isIPad || DeviceProps.isIPhone) { // on iOS Safari the natively shared url is the canonical url
5840 var base = ($('head link[rel="canonical"]').attr('href') || "").split("#")[0] || "";
5841 var textPart = (location.href.split("#")[1] || "").replace(/\./g, "%2e"); // '.' -> '%2e'; otherwise it may be interpreted as info text when shared
5842 $('head link[rel="canonical"]').attr('href', textPart ? base + "#" + textPart : base );
5843 } else { // remove canonical url to use the real url for url sharing
5844 $('head link[rel="canonical"]').remove();
5845 }
5846 }
5847
5848 function removeTextFromLocation(){
5849 var url = location.href;
5850 if (url.search('#')>=0){
5851 window.history.pushState(null, "", url.split('#',2)[0]); // remove the share part of the url
5852 }
5853 updateCanonical();
5854 }
5855
5856 function applyUrl(url){
5857 // check initial translation in url
5858 dbg && console.log(TAG, "apply url", url);
5859 var translationPart = url.split('#',2)[1];
5860 if (translationPart){
5861 updateCanonical();
5862 var decodedText = _dec(translationPart);
5863 dbg && console.log(TAG, "decodedText", decodedText);
5864
5865 const parts = decodedText.split("{_DELIM_1_}")
5866 const givenLang1 = parts.shift()
5867 const givenLang2 = parts.shift()
5868
5869 var sourceText = "";
5870 var targetText = "";
5871 var hasTargetText = false;
5872 var targetSentenceTexts = [];
5873 while (parts.length){
5874 var s_t_sentences = parts.shift().split("{_DELIM_2_}", 2);
5875 dbg && console.log(TAG, "s_t_sentences:", s_t_sentences);
5876 sourceText += s_t_sentences[0];
5877 var t_sentence = s_t_sentences[1] || "";
5878 targetText += t_sentence;
5879 hasTargetText |= t_sentence.length > 0;
5880 targetSentenceTexts.push(t_sentence);
5881 }
5882 if (sourceText.length){
5883 _updateTextareaText($sourceEdit, "");
5884 $targetEdit.val(""); // Note: _updateTextareaText is not robust when used for multiple areas in one loop
5885 checkForChangedSourceEdit(); // to reset its internal state; only necessary as checkForChangedSourceEdit does not check for changed states
5886 stateManagement.sanitizeAndUpdateActiveLanguages (givenLang1, givenLang2);
5887
5888 if (hasTargetText){
5889 dbg && console.log(TAG, "Setting target sentence texts...");
5890 $targetEdit.val(targetText); // Note: updateTextareaText is not robust when used for multiple areas in one loop
5891 const rqContext = new RequestContext(webTranslatorCore.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED);
5892 rqContext[REQUEST_CONTEXT__TARGET_IS_PREDEFINED] = true;
5893 rqContext.sourceLangHasBeenDetermined = true;
5894 rqContext[REQUEST_CONTEXT__ON_SOURCE_SENTENCES_UPDATED].push(function(){
5895 var state = rqContext[REQUEST_CONTEXT__STATE];
5896 log(TAG, "Source sentences updated; Setting target sentences....");
5897 log(TAG, state[STATE__TARGET_SENTENCES]);
5898 state[STATE__TARGET_SENTENCES].splice(0);
5899 targetSentenceTexts.forEach(function(tSentenceText){
5900 var tSentence = createTargetSentence(tSentenceText);
5901 tSentence[T_SENTENCE__EDIT_REV] = counter();
5902 state[STATE__TARGET_SENTENCES].push(tSentence);
5903 });
5904 addRevertToOriginalHint();
5905
5906 });
5907
5908 dbg && console.log(TAG, "Setting source text:", sourceText);
5909 resetSourceText(rqContext, sourceText);
5910 checkForChangedTargetText('update');
5911 }else{
5912 dbg && console.log(TAG, "Setting source text:", sourceText);
5913 const rqContext = new RequestContext(webTranslatorCore.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED);
5914 // rqContext.sourceLangHasBeenDetermined = true;
5915 resetSourceText(rqContext, sourceText);
5916 }
5917 }
5918 }
5919 }
5920 function _dec(encodedText){
5921 return decodeURIComponent(encodedText).replace(/[\\]./g, function(match){return match==='\\\\' ? "{_BACKSLASH_}" : match;})
5922 .replace(/([^\\])[/]/g, "$1{_DELIM_1_}")
5923 .replace(/([^\\])[|]/g, "$1{_DELIM_2_}")
5924 .replace(/\\([/|])/g, "$1")
5925 .replace(/{_BACKSLASH_}/g,"\\")
5926 }
5927
5928 function _enc(rawText){
5929 return encodeURIComponent(rawText.replace(/([\\|/])/g, "\\$1"));
5930 }
5931 function createTextURL(state, includeTarget){
5932 var url = location.href.split("#",2)[0];
5933 var texts = [];
5934 var urlLength = url.length;
5935 var shortened = false;
5936 var fullTargetText = "";
5937 if (includeTarget){
5938 state[STATE__SOURCE_SENTENCES].some(function(sSentence, i){
5939 var sourceText = _enc(sSentence.getText());
5940 if (urlLength+sourceText.length>MAX_TEXT_LENGTH){
5941 shortened = true;
5942 return true;
5943 }
5944 var targetSentence = state[STATE__TARGET_SENTENCES][i];
5945 var targetText = (targetSentence && _enc(targetSentence.getText())) || "[...]";
5946 if (urlLength+targetText.length>MAX_TEXT_LENGTH){
5947 shortened = true;
5948 return true;
5949 }
5950 fullTargetText += (targetSentence && targetSentence.getText()) || "[...]";
5951 urlLength += sourceText.length + targetText.length + 2;
5952 texts.push(sourceText + encodeURIComponent("|") + targetText);
5953 return false;
5954 });
5955 } else {
5956 state[STATE__SOURCE_SENTENCES].some(function(sSentence, i){
5957 var sourceText = _enc(sSentence.getText());
5958 if (urlLength+sourceText.length>MAX_TEXT_LENGTH){
5959 shortened = true;
5960 return true;
5961 }
5962 var targetSentence = state[STATE__TARGET_SENTENCES][i];
5963
5964 fullTargetText += (targetSentence && targetSentence.getText()) || "[...]";
5965
5966 urlLength += sourceText.length;
5967 texts.push(sourceText);
5968 return false;
5969 });
5970 }
5971 if (texts.length && state.getSourceLang() && state.getTargetLang()){
5972 url += "#" + state.getSourceLang().toLowerCase() + "/" + state.getTargetLang().toLowerCase() + "/" + (includeTarget ? texts.join('/') : texts.join(''));
5973 }
5974 return [url, shortened, fullTargetText];
5975 }
5976 function targetTextHasBeenAltered(state){
5977 var changeFound = false;
5978 state[STATE__TARGET_SENTENCES].forEach(function(tSentence){
5979 var translation = tSentence.getTranslation();
5980 if (translation)
5981 changeFound |= (tSentence.getText().trim() != (translation[TRANSLATION__INITIAL_TRANSLATION_TEXT]||"").trim());
5982 });
5983 return changeFound;
5984 }
5985 function revertTargetText(){
5986 if (webTranslator.hasPendingRequests()){
5987 log(TAG, "Delayed revert...");
5988 webTranslator.hasPendingRequests.onValueChanged.push(function(){
5989 setTimeout(revertTargetText, 100);
5990 return 'REMOVE';
5991 });
5992 }else{
5993 log(TAG, "Revert!");
5994 var t = "";
5995 var state = webTranslatorCore.getActiveState();
5996 state[STATE__TARGET_SENTENCES].forEach(function(tSentence){
5997 var translation = tSentence.getTranslation();
5998 let s;
5999 if (translation){
6000 s = tSentence.getWhitespacesBefore() + translation[TRANSLATION__INITIAL_TRANSLATION_TEXT] + tSentence.getWhitespacesAfter();
6001 tSentence.setText(s);
6002 }else {
6003 s = tSentence.getText();
6004 }
6005 t += s;
6006 });
6007 log(TAG,"Reverted text ["+t+"]");
6008 updateTargetEditText(t);
6009 $('.lmt__translations_as_text__main_translation').text(t);
6010 refreshUrl();
6011 }
6012 }
6013 window._createTextUrl = function(includeTarget){
6014 return createTextURL(webTranslatorCore.getActiveState(), includeTarget);
6015 }
6016 window.onhashchange = function(){
6017 dbg && console.log(TAG, "window.onhashchange", location.href);
6018 applyUrl(location.href);
6019 };
6020 applyUrl(location.href);
6021
6022 function refreshUrl(){
6023 dbg && console.log(TAG, "Refresh URL...");
6024 if (isPro || $sourceEdit.val().length > MAX_TEXT_LENGTH){
6025 removeTextFromLocation();
6026 } else {
6027 var state = webTranslatorCore.getActiveState();
6028 if (targetTextHasBeenAltered(state)){
6029 removeTextFromLocation();
6030 } else {
6031 var url_shortened_targetText = createTextURL(webTranslatorCore.getActiveState(), false);
6032 if (url_shortened_targetText[1]){
6033 removeTextFromLocation();
6034 } else {
6035 var url = url_shortened_targetText[0].replace(/%20/g," ");
6036 window.history.replaceState(null, "", url);
6037 updateCanonical();
6038 dbg && console.log(TAG, "Update URL", url);
6039 }
6040 }
6041 }
6042 }
6043 on(ON.ON__SOURCE_TEXT_CHANGED, function(_rqContext){
6044 dbg && console.log(TAG, "CHANGED (0)");
6045 U.setTimeout_consolidated("share_refresh", refreshUrl, 500);
6046 });
6047 on(ON.ON__SOURCE_SENTENCES_CHANGED, function(_rqContext){
6048 dbg && console.log(TAG, "CHANGED (1)");
6049 U.setTimeout_consolidated("share_refresh", refreshUrl, 500);
6050 });
6051 on(ON.ON__TARGET_EDIT_TEXT_CHANGED, function(_reason){
6052 dbg && console.log(TAG, "CHANGED (2)");
6053 U.setTimeout_consolidated("share_refresh", refreshUrl, 500);
6054 });
6055
6056 // ---- ui: revert hint
6057 function addRevertToOriginalHint(){
6058 var removeHint = U.createCallOnce();
6059
6060 var $hint = $("<div class='lmt__altered_shared_text_hint' dl-test='translator-altered-text-hint'></div>")
6061 .append( $("<p>" + getLocalizedText('translator/share.editedTranslationHint', "The translation has been edited.") + "</p>") )
6062 .append( $("<div class='lmt__altered_shared_text_hint__buttons'></div>")
6063 .append( $("<button>" + getLocalizedText('translator/share.editedTranslation_revertButton', "Change back to the original version") + "</button>").click(function(){
6064 revertTargetText();
6065 removeHint();
6066 }))
6067 .append( $("<button>" + getLocalizedText('translator/share.editedTranslation_acceptButton', "Accept changes") + "</button>").click(removeHint) )
6068 )
6069 .appendTo($(domElements.targetEditOuterContainer));
6070 var $mobileHint = $("<div class='lmt__mobile_altered_shared_text_hint'></div>")
6071 .append( $("<p>" + getLocalizedText('translator/share.editedTranslationHint', "The translation has been edited.") + "</p>") )
6072 .append( $("<div class='lmt__mobile_altered_shared_text_hint__buttons'></div>")
6073 .append( $("<button>" + getLocalizedText('translator/share.editedTranslation_revertButton', "Change back to the original version") + "</button>").click(function(){
6074 revertTargetText();
6075 removeHint();
6076 }))
6077 .append( $("<button>" + getLocalizedText('translator/share.editedTranslation_acceptButton', "Accept changes") + "</button>").click(removeHint) )
6078 )
6079 .insertAfter($('.lmt__translations_as_text'));
6080
6081 removeHint.push(function(){
6082 $hint.css('opacity', "0");
6083 $mobileHint.css('opacity', "0");
6084 setTimeout(function(){ $hint.remove(); $mobileHint.remove(); }, 400);
6085 });
6086 setTimeout(function(){
6087 on(ON.ON__TARGET_EDIT_TEXT_CHANGED, function(){
6088 log(TAG, "removeHint 1");
6089 removeHint();
6090 return 'REMOVE';
6091 })
6092 on(ON.ON__SOURCE_TEXT_CHANGED, function(){
6093 log(TAG, "removeHint 2");
6094 removeHint();
6095 return 'REMOVE';
6096 });
6097 });
6098 }
6099
6100 return {
6101 createTextURL,
6102 targetTextHasBeenAltered
6103 }
6104});
6105// = == =============== End: Share feature =============== == =
6106
6107
6108
6109// = == =============== Feature: observeUserEdits =============== == =
6110/*
6111 Provides a callback triggered when a target sentence is updated by selecting an alternative word, autocompletion or
6112 by selecting a different sentence after updating by keyboard.
6113
6114 {
6115 onUserEdit: MultiProcedure(editInfo{
6116 state: State,
6117 targetSentence: TargetSentence,
6118 type: 'EDIT_TYPE__SELECT_ALTERNATIVE' | 'EDIT_TYPE__AUTOCOMPLETION' | 'EDIT_TYPE__MANUAL_EDIT_TIMEOUT' | 'EDIT_TYPE__SELECT_ALTERNATIVE_FIRST_STEP'
6119 })
6120 }
6121*/
6122webTranslator.features.define('observeUserEdits',
6123 ['observeSentenceSelection', 'webTranslatorCore'],
6124 function _feature__observeUserEdits(observeSentenceSelection, webTranslatorCore){
6125 const TAG = "[observeUserEdits]";
6126 const dbg = dlPageState.devMode && false;
6127
6128 var pendingTSentence, stateForPendingTSentence;
6129 var cancelCommitTimer = U.createCallOnce();
6130
6131 observeSentenceSelection.onTargetSelectionChanged.push(function(tSentencePos_selectionBegin, _tSentencePos_selectionEnd){
6132 dbg && console.log(TAG, "onTargetSelectionChanged", tSentencePos_selectionBegin);
6133 cancelCommitTimer();
6134
6135 if (pendingTSentence && stateForPendingTSentence){
6136 if (!tSentencePos_selectionBegin) { // unselect --> commit after some time (the pending sentence could be re-selected in the meantime)
6137 var t = setTimeout(function(){
6138 pendingTSentence.takeSnapshot(true); // make sure the current text is registered as full snapshot
6139 commit({
6140 state: stateForPendingTSentence,
6141 targetSentence: pendingTSentence,
6142 type: 'EDIT_TYPE__MANUAL_EDIT_TIMEOUT'
6143 });
6144 pendingTSentence = undefined;
6145 stateForPendingTSentence = undefined;
6146 }, 5000);
6147 cancelCommitTimer.push(function(){ clearTimeout(t); });
6148 } else if (tSentencePos_selectionBegin.sentence!==pendingTSentence){ // different sentence selected --> commit now
6149 pendingTSentence.takeSnapshot(true); // make sure the current text is registered as full snapshot
6150 commit({
6151 state: stateForPendingTSentence,
6152 targetSentence: pendingTSentence,
6153 type: 'EDIT_TYPE__MANUAL_EDIT_TIMEOUT'
6154 });
6155 pendingTSentence = tSentencePos_selectionBegin.sentence;
6156 stateForPendingTSentence = webTranslatorCore.getActiveState();
6157 } else {
6158 // selection changed inside pendingTSentence --> do nothing
6159 }
6160 } else if (tSentencePos_selectionBegin){
6161 pendingTSentence = tSentencePos_selectionBegin.sentence;
6162 stateForPendingTSentence = webTranslatorCore.getActiveState();
6163 }
6164 });
6165 on(ON.ON__USER_EDIT_PERFORMED, function(editInfo){
6166 dbg && console.log(TAG, editInfo);
6167 commit(editInfo);
6168 });
6169
6170 var latestedCommittedEditRev = -1;
6171
6172 var onUserEdit = U.createMultiProcedure();
6173 function commit(editInfo){
6174 var tSentence = editInfo.targetSentence;
6175 dbg && console.log(TAG, "COMMIT", tSentence);
6176
6177 if (tSentence[T_SENTENCE__EDIT_REV] <= latestedCommittedEditRev){
6178 dbg && console.log(TAG, "Unchanged or already handled");
6179 return;
6180 }
6181
6182 var finalTargetText = tSentence.getNormalizedText();
6183 var translation = tSentence.getTranslation();
6184
6185 if (!translation){
6186 return;
6187 }
6188 var initialTargetText = translation[TRANSLATION__INITIAL_TRANSLATION_TEXT].trim().replace(REGEX_collapseWhitspaces, " ");
6189 if (finalTargetText===initialTargetText){
6190 dbg && console.log(TAG, "unchanged");
6191 return;
6192 }
6193
6194 latestedCommittedEditRev = tSentence[T_SENTENCE__EDIT_REV];
6195
6196 onUserEdit(editInfo);
6197 }
6198 return {
6199 onUserEdit: onUserEdit
6200 };
6201});
6202// = == =============== end: observeUserEdits =============== == =
6203
6204// = == =============== Feature: edit logs =============== == =
6205/*
6206 Collect user edits of free users for general model improvements.
6207 */
6208config[CONFIG__SEND_EDIT_LOGS] && webTranslator.features.define('editLogs',
6209 ['observeUserEdits'], function _feature__editStats(observeUserEdits){
6210 const TAG = "[editLogs]";
6211 const dbg = dlPageState.devMode && true;
6212
6213 function _editDiff(sourceParts, targetParts){
6214 var remainingParts_source = sourceParts.slice(0);
6215 var remainingParts_target = targetParts.slice(0);
6216 var diffParts = [];
6217 while (remainingParts_source.length && remainingParts_target.length){
6218 var sourcePart = remainingParts_source.shift();
6219 var targetPart = remainingParts_target.shift();
6220 diffParts.push(targetPart);
6221 if (sourcePart.text!==targetPart.text){
6222 targetPart._prevText = sourcePart.text;
6223 break;
6224 }
6225 }
6226 while (remainingParts_target.length){
6227 diffParts.push(remainingParts_target.shift());
6228 }
6229 return diffParts;
6230 }
6231 function calculateEdits(texts){
6232 var remainingSentenceParts = (texts||[]).map(splitTextIntoParts);
6233 var resultParts = remainingSentenceParts.pop();
6234 while (remainingSentenceParts.length){
6235 resultParts = _editDiff(remainingSentenceParts.pop(), resultParts);
6236 }
6237 return resultParts.map(function(part){
6238 return part._prevText===undefined ? part.text : "{{" + part._prevText + "->" + part.text + "}}";
6239 }).join("");
6240 }
6241 let editSessionId = 0;
6242 function _resetSourceTextId(){editSessionId = Math.floor(Math.random()*100000000);}
6243 on(ON.ON__SOURCE_TEXT_EMPTY, _resetSourceTextId);
6244 on(ON.ON__LANGUAGE_CHANGED, _resetSourceTextId);
6245
6246 var pendingEdits = [];
6247 let timoutTimer = 0;
6248 observeUserEdits.onUserEdit.push(function(editInfo){ // delay sending of edit logs in case the user makes several changes to the sentence
6249 if (editInfo.type !== 'EDIT_TYPE__SELECT_ALTERNATIVE_FIRST_STEP'){ // ignore incomplete edits... fillup request is pending
6250 clearTimeout(timoutTimer);
6251 if (pendingEdits.length && pendingEdits[pendingEdits.length-1].targetSentence === editInfo.targetSentence){
6252 pendingEdits[pendingEdits.length-1] = editInfo;
6253 } else {
6254 commit();
6255 pendingEdits.push(editInfo);
6256 }
6257
6258 if (pendingEdits.length > 1 || editInfo.type === 'EDIT_TYPE__MANUAL_EDIT_TIMEOUT'){
6259 commit();
6260 } else {
6261 timoutTimer = setTimeout(commit, 20000);
6262 }
6263 }
6264 });
6265 let editCounter;
6266 function commit(){
6267 while (pendingEdits.length){
6268 var editInfo = pendingEdits.shift();
6269
6270 editCounter || (editCounter = U.persistentValue('LMT.ec', 0, 60*60*24*30)); // count user edits (user experience); timeout after a month
6271 editCounter( (editCounter()||0) + 1);
6272
6273 var state = editInfo.state;
6274 var targetSentence = editInfo.targetSentence;
6275 var sourceSentence = targetSentence.getSourceSentence();
6276 if (sourceSentence){
6277 var context = doGetSourceContext(sourceSentence);
6278 var statJob = {
6279 log_kind: "edit",
6280 initial_target_text: targetSentence[T_SENTENCE__EDIT_SNAPSHOTS][0].text,
6281 final_target_text: targetSentence.getNormalizedText(),
6282 target_edits: calculateEdits(targetSentence[T_SENTENCE__EDIT_SNAPSHOTS].map(snapshot => snapshot.text)),
6283 source_text: sourceSentence.getNormalizedText(),
6284 source_context_before: context.prefix,
6285 source_context_after: context.postfix,
6286 edit_session_id: editSessionId,
6287 user_experience: Math.floor(Math.log2(editCounter())),
6288 source_lang: state.getSourceLang(),
6289 target_lang: state.getTargetLang(),
6290 };
6291 dbg && console.log(TAG, JSON.stringify(statJob, false, 4));
6292
6293 // Todo: this has to be refactors to work again...
6294 // false && rpc_jobBackend.getFunction('LMT_handleAdditionalData')({
6295 // data: statJob
6296 //});
6297 }
6298
6299 }
6300 }
6301});
6302
6303// = == =============== End: editLogs =============== == =
6304
6305// = == =============== Feature: dev_retranslate =============== == =
6306webTranslator.features.define('dev_retranslate', ['webTranslatorCore', 'translationBackend', 'stateManagement'],
6307 function _Feature__dev_retranslate(webTranslatorCore, translationBackend, stateManagement){
6308 return function _doRetranslate(){
6309 const sourceLang = webTranslatorCore.sourceLang();
6310 const targetLang = webTranslatorCore.targetLang();
6311 translationBackend.lmtBackends['translation']().dev_clearTranslationCache();
6312 stateManagement.updateActiveLanguages("", ""); // reset languages
6313 stateManagement.updateActiveLanguages(sourceLang, targetLang);
6314 // notify(ON.ON__SOURCE_TEXT_CHANGED, new RequestContext(webTranslatorCore.getActiveState(), TRIGGER__SOURCE_LANG_CHANGED));
6315 };
6316});
6317// = == =============== end: dev_retranslate =============== == =
6318
6319
6320// ------------------------------
6321// = == =============== Temporary hacks =============== == =
6322
6323
6324setTimeout(function(){
6325 // todo: move to docTrans
6326 LMT_UI_Tooltip.initTooltips($(".docTrans_translator_upload_button > form > div")); //
6327});
6328
6329
6330// = == =============== Temporary hacks =============== == =
6331
6332
6333// = == =============== appScrollSynchronization =============== == =
6334
6335config['scrollMode'] === 'app' && webTranslator.features.define('appScrollSynchronization',
6336 ['observeSentenceSelection', 'domElements', 'webTranslatorCore'],
6337 function _feature__appScrollSynchronization(observeSentenceSelection, domElements, webTranslatorCore){
6338 const dbg = false;
6339 const TAG = "[appScrollSynchronization]";
6340
6341 dbg && console.log(TAG, "init");
6342
6343 const SCROLL_START_TIME_DELAY = 125; // delay for container-height animation in case they overlap
6344
6345 observeSentenceSelection.onTargetSelectionChanged.push( (sentencePos_selectionBegin, sentencePos_selectionEnd)=>{
6346 dbg && console.log(TAG, sentencePos_selectionBegin, sentencePos_selectionEnd );
6347 if (sentencePos_selectionBegin && sentencePos_selectionBegin.sentence){
6348 const sSentence = sentencePos_selectionBegin.sentence.getSourceSentence();
6349 let sourceTextOffset = 0;
6350 for (let sSentenceCursor = sSentence[S_SENTENCE__PREV_SENTENCE]; sSentenceCursor; sSentenceCursor = sSentenceCursor[S_SENTENCE__PREV_SENTENCE]){
6351 sourceTextOffset += sSentenceCursor.getText().length;
6352 }
6353 // sourceTextOffset =- 1; // skip the potential linebreak of the last sentence
6354 dbg && console.log(TAG, "sourceTextOffset", sourceTextOffset);
6355
6356 const sentencePixelOffset = LMT_UI_Utils.getTextareaPixelOffsetForCursorOffset(domElements.sourceEdit, sourceTextOffset, false);
6357 //domElements.sourceEditInnerContainer.scrollTop = sentencePixelOffset.top;
6358 dbg && console.log(TAG, 'sentencePixelOffset', sentencePixelOffset);
6359
6360 setTimeout(() => LMT_UI_Utils.smoothScroll(domElements.sourceEditInnerContainer, sentencePixelOffset.top), SCROLL_START_TIME_DELAY);
6361 }
6362 });
6363
6364 observeSentenceSelection.onSourceSelectionChanged.push( (sentencePos_selectionBegin, sentencePos_selectionEnd)=>{
6365 const state = webTranslatorCore.getActiveState();
6366
6367 dbg && console.log(TAG, sentencePos_selectionBegin, sentencePos_selectionEnd );
6368 if (state && sentencePos_selectionBegin && sentencePos_selectionBegin.sentence){
6369 const tSentences = state.getTargetSentences();
6370
6371 const tSentence = tSentences[sentencePos_selectionBegin.sentenceIndex];
6372 if (tSentence){
6373 let targetTextOffset = 0;
6374 for (let tSentenceIndex = sentencePos_selectionBegin.sentenceIndex - 1; tSentenceIndex>=0; --tSentenceIndex){
6375 targetTextOffset += tSentences[tSentenceIndex].getText().length;
6376 }
6377 dbg && console.log(TAG, "targetTextOffset", targetTextOffset);
6378
6379 const sentencePixelOffset = LMT_UI_Utils.getTextareaPixelOffsetForCursorOffset(domElements.targetEdit, targetTextOffset, false);
6380 // domElements.targetEditInnerContainer.scrollTop = sentencePixelOffset.top;
6381 dbg && console.log(TAG, 'sentencePixelOffset', sentencePixelOffset);
6382
6383 setTimeout(() => LMT_UI_Utils.smoothScroll(domElements.targetEditInnerContainer, sentencePixelOffset.top), SCROLL_START_TIME_DELAY);
6384 }
6385 }
6386 });
6387
6388 //
6389}); // ==== end: appScrollSynchronization
6390
6391// password.addEventListener('focus', (event) => {
6392
6393
6394// = == =============== appTextareaResize =============== == =
6395config['scrollMode'] === 'app' && webTranslator.features.define('appTextareaResize',
6396 ['domElements', 'eventHandling'],
6397 (domElements, eventHandling) => {
6398
6399 const TAG = "[appTextareaResize]";
6400 const dbg = false;
6401
6402 // detect focus and guess if texts fits into normal area without scrolling
6403 const condition_sourceGotFocus = U.value(false);
6404 const condition_targetGotFocus = U.value(false);
6405 const condition_textFitsOnlyWithScrolling = U.value(false);
6406 domElements.sourceEdit.addEventListener('focus', ()=>{
6407 const textHeight = LMT_UI_Utils.getTextareaPixelOffsetForCursorOffset(domElements.sourceEdit, 100000, false).top;
6408 condition_textFitsOnlyWithScrolling(textHeight > window.innerHeight*0.5-120); // guess half window height minus language selection and stuff
6409 dbg && console.log(TAG, "textHeight", textHeight);
6410 condition_sourceGotFocus(true);
6411 condition_targetGotFocus(false);
6412 });
6413 domElements.targetEdit.addEventListener('focus', ()=>{
6414 const textHeight = LMT_UI_Utils.getTextareaPixelOffsetForCursorOffset(domElements.sourceEdit, 100000, false).top; // use sourceEdit text as reference (target text may be delayed)
6415 condition_textFitsOnlyWithScrolling(textHeight > window.innerHeight*0.5-230); // guess (takes button-height into account)
6416 dbg && console.log(TAG, "textHeight", textHeight);
6417 condition_sourceGotFocus(false);
6418 condition_targetGotFocus(true);
6419 });
6420
6421 // requires more than one sentence (prevents being used when alternatives are shown)
6422 const condition_numberOfSentences = U.value(false);
6423 eventHandling.on(eventHandling.ON.ON__SOURCE_TEXT_EMPTY, ()=>condition_numberOfSentences(false));
6424 eventHandling.on(eventHandling.ON.ON__SOURCE_SENTENCES_CHANGED,
6425 rqContext=>condition_numberOfSentences(rqContext.state.getSourceSentences().length>1));
6426
6427 // don't show with too small or too large windows
6428 const condition_windowHeight = U.value();
6429 const checkWindowHeight = () => condition_windowHeight(window.innerWidth>350 && window.innerHeight<800);
6430 window.addEventListener('resize', checkWindowHeight);
6431 checkWindowHeight();
6432
6433 const enlargementClass = U.value("");
6434
6435 U.withValue([condition_sourceGotFocus, condition_targetGotFocus, condition_textFitsOnlyWithScrolling, condition_numberOfSentences, condition_windowHeight],
6436 (sourceFocus, targetFocus, ...conditions) => {
6437 dbg && console.log(TAG, "conditions", sourceFocus, targetFocus, ...conditions);
6438 if (conditions.every(condition=>condition)){
6439 if (sourceFocus){
6440 enlargementClass("lmt--enlargedSource");
6441 }
6442 else if (targetFocus){
6443 enlargementClass("lmt--enlargedTarget");
6444 }
6445 else{
6446 enlargementClass("");
6447 }
6448 }
6449 else {
6450 enlargementClass("");
6451 }
6452 });
6453 {
6454 const removeClass = U.createCallOnce();
6455 U.withValue(enlargementClass, c => {
6456 removeClass();
6457 if (c){
6458 domElements.rootContainer.classList.add(c);
6459 removeClass.push(()=>domElements.rootContainer.classList.remove(c));
6460 }
6461 dbg && console.log(TAG, "enlargementClass", c);
6462 });
6463 }
6464});
6465
6466// = == =============== end: appTextareaResize =============== == =
6467
6468
6469// = == =============== ABTestController_clickOnWordHint =============== == =
6470M.requireAsync(['Tracker', 'dateTime'], (Tracker, dateTime) => {
6471 webTranslator.features.defineFeature_lazy(
6472 'ABTestController_clickOnWordHint',
6473 ['eventHandling', 'webTranslatorCore'], ({on, ON}, webTranslatorCore) => {
6474
6475 // configuration
6476 const trackerType = 'abTest';
6477 const trackerCollectIntervalInDays = 3;
6478 const testVersion = 3;
6479
6480 return class {
6481 constructor(testName, testCaseName) {
6482 this._testName = testName;
6483 this._testCaseName = testCaseName;
6484
6485 this._isTrackerActive = U.persistentValue(`${this._testName}.isTrackerActive`, false, dateTime.secondsPerDay * 30);
6486 this._tracker = this._isTrackerActive() ? this._instantiateTracker() : null;
6487
6488 if (this._tracker && this._shouldTrackerCollectData()) {
6489 this._isTrackerActive(false);
6490 this._tracker.collectData();
6491 this._tracker = null;
6492 }
6493
6494 this._registerEventHandlers();
6495 }
6496
6497 incrementCounter(counterName) {
6498 this._tracker && this._tracker.incrementCounter(counterName);
6499 }
6500
6501 trackEvent(eventName, eventValue = true) {
6502 this._tracker && this._tracker.trackEvent(eventName, eventValue);
6503 }
6504
6505 _registerEventHandlers() {
6506 on(ON.ON__INITIAL_SOURCE_TRANSLATION_UPDATED, () => {
6507 // only after first translation (dlStats.getStats() => 'h'-parameter),
6508 // can be decided if tracker should be activated
6509 if (!this._tracker && this._shouldActivateTracker()) {
6510 this._isTrackerActive(true);
6511 this._tracker = this._instantiateTracker(true);
6512 }
6513
6514 this._tracker && this._tracker.trackEvent('translatorUsed', true);
6515
6516 return 'REMOVE';
6517 });
6518 }
6519
6520 _shouldActivateTracker() {
6521 const daysOfTranslatorUse = parseInt(dlStats.getStats('h', 0)(), 10) || 0;
6522 const isFirstTranslatorVisit = daysOfTranslatorUse === 1;
6523 return isFirstTranslatorVisit && webTranslatorCore.__abTest_clickOnWordHint_trackingEnabled;
6524 }
6525
6526 _instantiateTracker(addMetaInfo = false) {
6527 const tracker = new Tracker(trackerType, this._testName);
6528
6529 if (addMetaInfo) {
6530 tracker.setMetaKeyValue('testName', this._testName);
6531 tracker.setMetaKeyValue('testCaseName', this._testCaseName);
6532 tracker.setMetaKeyValue('testVersion', testVersion);
6533 }
6534
6535 return tracker;
6536 }
6537
6538 _shouldTrackerCollectData() {
6539 const lastCollected = this._tracker.getLastCollectedDate();
6540 const dateToday = new Date();
6541 const diffDays = dateTime.getDatesDifferenceInDays(lastCollected, dateToday);
6542 return diffDays >= trackerCollectIntervalInDays;
6543 }
6544 };
6545 });
6546});
6547// = == =============================== end: ABTestController_clickOnWordHint ================================ == =
6548
6549
6550// = == =============================== ClickOnWordHint_classic ================================ == =
6551webTranslator.features.defineFeature_lazy(
6552 'ClickOnWordHint_classic',
6553 ['eventHandling', 'localizedTexts', 'domElements', 'smartTargetEditing', 'ABTestController_clickOnWordHint'],
6554 ({on, ON}, localizedTexts, domElements, smartTargetEditing, ABTestController_clickOnWordHint) => {
6555 const $targetEdit = $(domElements.targetEdit);
6556 const $targetEditOuterContainer = $(domElements.targetEditOuterContainer);
6557 const $rootContainer = $(domElements.rootContainer);
6558
6559 const translation = localizedTexts.get('translator/messageBox_clickHint', "Click on a word to get alternative formulations.");
6560 const hintBox = new LMT_UI_MessageBox2(translation);
6561 const hintBoxShownMaximum = 10;
6562 const showHintBoxDelay = 5 * 1000;
6563 const hintBoxShownCounter = U.persistentValue('clickOnWordHint.hintBoxShownCounter', 0);
6564 const isHintDisabled = U.persistentValue('clickOnWordHint.hintBoxDisabled', false);
6565
6566 { // =================== TODO: remove after October 2019 ======================
6567 // if there is a 'clickHint' property under 'LMT_MessageBox.messageBoxes' in the local storage
6568 // then read clickHint.displayCount from local storage, if its value is greater 10,
6569 // then set the new persistant value 'clickOnWordHint.hintBoxDisabled'
6570 // in any case delete the old clickHint property from local storage
6571 const messageBoxesStore = U.persistentValue('LMT_MessageBox.messageBoxes', undefined);
6572 const messageBoxesObj = messageBoxesStore();
6573
6574 if (messageBoxesObj && messageBoxesObj.hasOwnProperty('clickHint')) {
6575 if (messageBoxesObj.clickHint.hasOwnProperty('displayCount')) {
6576 const displayCount = parseInt(messageBoxesObj.clickHint.displayCount, 10);
6577 if (displayCount >= hintBoxShownMaximum) {
6578 isHintDisabled(true);
6579 }
6580 }
6581 delete messageBoxesObj.clickHint;
6582 messageBoxesStore(JSON.parse(JSON.stringify(messageBoxesObj)));
6583 }
6584 } // =============================== REMOVE end ===================================
6585
6586 const abTest = new ABTestController_clickOnWordHint('clickOnWordHint', 'classicHintBox');
6587
6588 function updateTranslationMessageBox() {
6589 const targetEditOffset = $targetEdit.offset();
6590 const outerWidth = $targetEdit.outerWidth();
6591 const outerHeight = $targetEditOuterContainer.outerHeight();
6592 const hintBoxWidth = 250;
6593
6594 targetEditOffset.top += outerHeight - 12;
6595 targetEditOffset.left += (outerWidth / 2) - (hintBoxWidth / 2);
6596
6597 hintBox.setOffset(targetEditOffset);
6598
6599 if ($rootContainer.hasClass("lmt--showing_alternatives")) {
6600 hintBox.hide();
6601 }
6602 }
6603
6604 function shouldShowHintBox() {
6605 const REGEX_sourceTextIsSentence = /[.!?:]\s*$/;
6606 const theText = $sourceEdit.val();
6607
6608 return !globalState[GSTATE__EDIT_FEATURES_DISABLED]() &&
6609 !isHintDisabled() &&
6610 hintBoxShownCounter() < hintBoxShownMaximum &&
6611 theText.length > 50 &&
6612 $targetEdit.val().length > 0 &&
6613 !smartTargetEditing.isPopupOpen() &&
6614 REGEX_sourceTextIsSentence.test(theText) &&
6615 !$rootContainer.hasClass("lmt--showing_alternatives") &&
6616 !$(".lmt__language_select.lmt__language_select--open").length &&
6617 !$(".lmt__edit_text_popup--initial_alternatives").length;
6618 }
6619
6620 function showHintBoxCallback() {
6621 if (hintBox.isHidden() && shouldShowHintBox()) {
6622 hintBoxShownCounter(hintBoxShownCounter() + 1);
6623 abTest.incrementCounter('hintBoxShown');
6624 updateTranslationMessageBox();
6625 hintBox.show();
6626 }
6627 }
6628
6629 $(window).on('resize', () => {
6630 if (!hintBox.isHidden()) {
6631 hintBox.hide();
6632 U.setTimeout_consolidated('showHintBoxAfterResizeTimeout', () => {
6633 updateTranslationMessageBox();
6634 hintBox.show();
6635 }, 600);
6636 }
6637 });
6638
6639 on(ON.ON__CLICK_TARGET_SENTENCE, () => {
6640 hintBox.hide();
6641 });
6642
6643 on(ON.ON__INITIAL_SOURCE_TRANSLATION_UPDATED, () => {
6644 if (hintBox.isHidden() && shouldShowHintBox()) {
6645 U.setTimeout_consolidated('clickWordHintBoxTimeoutToShow', showHintBoxCallback, showHintBoxDelay);
6646 } else {
6647 hintBox.hide();
6648 }
6649 });
6650
6651 on(ON.ON__USER_EDIT_PERFORMED, (editInfo) => {
6652 if (editInfo.type === 'EDIT_TYPE__SELECT_ALTERNATIVE') {
6653 abTest.incrementCounter('alternativeChosen');
6654 }
6655 });
6656
6657 on(ON.ON__TARGET_POPUP_OPENED, popupInfo => {
6658 if (popupInfo[TARGET_POPUP__ENTRIES] &&
6659 popupInfo[TARGET_POPUP__ENTRIES].length > 1 &&
6660 popupInfo[TARGET_POPUP__TYPE] === "alternatives") {
6661
6662 abTest.incrementCounter('alternativesShown');
6663 !isHintDisabled() && abTest.trackEvent('hintBoxDisabled', true);
6664
6665 isHintDisabled(true);
6666 hintBox.hide();
6667 }
6668 });
6669
6670 on(ON.ON__LANGUAGE_SELECT_OPENED, () => {
6671 hintBox.hide(250);
6672 });
6673 }
6674);
6675// = == =============================== end: ClickOnWordHint_classic ================================ == =
6676
6677
6678// = == =============================== ClickOnWordHint ================================ == =
6679webTranslator.features.defineFeature_lazy(
6680 'ClickOnWordHint',
6681 ['eventHandling', 'localizedTexts', 'domElements', 'smartTargetEditing', 'ABTestController_clickOnWordHint'],
6682 ({on, ON}, localizedTexts, domElements, smartTargetEditing, ABTestController_clickOnWordHint) => {
6683 // get jQuery objects
6684 const $targetEdit = $(domElements.targetEdit);
6685 const $rootContainer = $(domElements.rootContainer);
6686
6687 // instantiate hintBox
6688 const clickHintText = localizedTexts.get('translator/messageBox_clickHint', 'Click on a word to get alternative formulations.');
6689 const hintBox = new LMT_UI_MessageBox2(clickHintText);
6690 const hintBoxSize = hintBox.getBoxSize();
6691 const showHintBoxDelay = 3 * 1000;
6692 const showHintBoxAfterResizeTimeout = 0.6 * 1000;
6693
6694 // parameters for deciding if the hintBox should be shown
6695 const showHintBoxMaxTimes = 10;
6696 const alternativesShownMaxTimes = 5;
6697 const alternativeChosenMaxTimes = 2;
6698 const sourceTextMinLength = 51;
6699 const targetTextMinLength = 1;
6700 const REGEX_sourceTextIsSentence = /[.!?:]\s*$/;
6701
6702 // keep information in local storage for deciding if the hintBox should be shown
6703 const isHintBoxDisabled = U.persistentValue('clickOnWordHint.hintBoxDisabled', false);
6704 const hintBoxShownCounter = U.persistentValue('clickOnWordHint.hintBoxShownCounter', 0);
6705 const alternativesShownCounter = U.persistentValue('clickOnWordHint.alternativesShownCounter', 0);
6706 const alternativeChosenCounter = U.persistentValue('clickOnWordHint.alternativeChosenCounter', 0);
6707
6708 // on change of values, check if hintBox should be disabled
6709 U.withValue(
6710 [hintBoxShownCounter, alternativesShownCounter, alternativeChosenCounter],
6711 () => {
6712 if (!isHintBoxDisabled() && shouldDisableHintBox()) {
6713 isHintBoxDisabled(true);
6714 abTest.trackEvent('hintBoxDisabled', true);
6715 }
6716 }
6717 );
6718
6719 const abTest = new ABTestController_clickOnWordHint('clickOnWordHint', 'hintBoxBelowText');
6720
6721 function showHintBox() {
6722 if (doesHintBoxFitIntoTargetEdit()) {
6723 hintBox.setOffset(calculateHintBoxOffset());
6724 hintBox.show();
6725 }
6726 }
6727
6728 function shouldShowHintBox() {
6729 const sourceText = $sourceEdit.val();
6730 const targetText = $targetEdit.val();
6731
6732 return hintBox.isHidden() &&
6733 !globalState[GSTATE__EDIT_FEATURES_DISABLED]() &&
6734 !smartTargetEditing.isPopupOpen() &&
6735 !isHintBoxDisabled() &&
6736 sourceText.length > sourceTextMinLength &&
6737 targetText.length > targetTextMinLength &&
6738 REGEX_sourceTextIsSentence.test(sourceText) &&
6739 !$rootContainer.hasClass('lmt--showing_alternatives') &&
6740 !$('.lmt__language_select.lmt__language_select--open').length &&
6741 !$('.lmt__edit_text_popup--initial_alternatives').length;
6742 }
6743
6744 function calculateOffsetBelowTargetText() {
6745 const textPixelOffset = LMT_UI_Utils.getTextareaPixelOffsetForCursorOffset($targetEdit, 1000 * 1000, false);
6746 const computedStyle = window.getComputedStyle($targetEdit.get(0));
6747 const lineHeight = parseInt(computedStyle.getPropertyValue('line-height'), 10);
6748
6749 return textPixelOffset.top + lineHeight;
6750 }
6751
6752 function calculateHintBoxOffset() {
6753 const top = $targetEdit.offset().top + calculateOffsetBelowTargetText();
6754 const left = $targetEdit.offset().left + parseInt($targetEdit.css('padding-left'), 10);
6755
6756 return { top, left };
6757 }
6758
6759 function doesHintBoxFitIntoTargetEdit() {
6760 const paddingTop = parseInt($targetEdit.css('padding-top'), 10);
6761 const targetEditOffsetFromTop = calculateOffsetBelowTargetText();
6762 const doesHeightFit = $targetEdit.height() + paddingTop > targetEditOffsetFromTop + hintBoxSize.height;
6763
6764 const rightMargin = 10; // this margin ensures there is some free space to the right of the hintbox
6765 const targetEditPaddingLeft = parseInt($targetEdit.css('padding-left'), 10);
6766 const doesWidthFit = $(window).width() > $targetEdit.offset().left + targetEditPaddingLeft + hintBoxSize.width + rightMargin;
6767
6768 return doesHeightFit && doesWidthFit;
6769 }
6770
6771 function shouldDisableHintBox() {
6772 return hintBoxShownCounter() >= showHintBoxMaxTimes ||
6773 alternativesShownCounter() >= alternativesShownMaxTimes ||
6774 alternativeChosenCounter() >= alternativeChosenMaxTimes;
6775 }
6776
6777 function showHintBoxCallback() {
6778 if (shouldShowHintBox()) {
6779 hintBoxShownCounter(hintBoxShownCounter() + 1);
6780 abTest.incrementCounter('hintBoxShown');
6781 showHintBox();
6782 }
6783 }
6784
6785 $(window).on('resize', () => {
6786 if (!hintBox.isHidden()) {
6787 hintBox.hide();
6788 U.setTimeout_consolidated('showHintBoxAfterResizeTimeout', showHintBox, showHintBoxAfterResizeTimeout);
6789 }
6790 });
6791
6792 on(ON.ON__CLICK_TARGET_SENTENCE, () => {
6793 hintBox.hide();
6794 });
6795
6796 on(ON.ON__INITIAL_SOURCE_TRANSLATION_UPDATED, () => {
6797 if (shouldShowHintBox()) {
6798 U.setTimeout_consolidated('showHintBoxTimeout', showHintBoxCallback, showHintBoxDelay);
6799 } else {
6800 hintBox.hide();
6801 }
6802 });
6803
6804 on(ON.ON__USER_EDIT_PERFORMED, (editInfo) => {
6805 if (editInfo.type === 'EDIT_TYPE__SELECT_ALTERNATIVE') {
6806 alternativeChosenCounter(alternativeChosenCounter() + 1);
6807 abTest.incrementCounter('alternativeChosen');
6808 }
6809 });
6810
6811 on(ON.ON__TARGET_POPUP_OPENED, popupInfo => {
6812 if (popupInfo[TARGET_POPUP__TYPE] === 'alternatives' &&
6813 popupInfo[TARGET_POPUP__ENTRIES] &&
6814 popupInfo[TARGET_POPUP__ENTRIES].length > 1) {
6815 hintBox.hide();
6816 alternativesShownCounter(alternativesShownCounter() + 1);
6817 abTest.incrementCounter('alternativesShown');
6818 }
6819 });
6820
6821 on(ON.ON__LANGUAGE_SELECT_OPENED, hintBox.hide);
6822 }
6823);
6824// = == =============================== end: ClickOnWordHint ================================ == =
6825
6826// = == =============== Additional features =============== == =
6827// Add features here if you want
6828
6829// ======================================================================================
6830
6831// = == =============================== FeatureInterface ================================ == =
6832/**
6833 * Interface that is required by optional translator-features.
6834 * Note: This interface is not versioned and should not be used by external components (like apps)
6835 */
6836webTranslator.features.define('featureInterface', ['webTranslatorCore', 'eventHandling'], (webTranslatorCore, {on,ON}) => {
6837 const featureInterface = {
6838 getFirstTargetSentence: function(){
6839 return ((webTranslatorCore.getActiveState() && webTranslatorCore.getActiveState()[STATE__TARGET_SENTENCES][0] && webTranslatorCore.getActiveState()[STATE__TARGET_SENTENCES][0][T_SENTENCE__TEXT]) || "").trim();
6840 },
6841 isPro: isPro,
6842 isSourceTextEmpty: webTranslatorCore.sourceIsEmpty,
6843 onAnyclick: U.createMultiProcedure(),
6844 getCopyFooter: function(lang){
6845 return COPY_FOOTERS[lang] || COPY_FOOTERS['EN'];
6846 },
6847 getTargetSentences: () => {
6848 return webTranslatorCore.getActiveState() ? webTranslatorCore.getActiveState()[STATE__TARGET_SENTENCES] : [];
6849 }
6850 };
6851
6852 on(ON.ON__ANYCLICK, function($targetElement, clickEvent){
6853 featureInterface.onAnyclick($targetElement, clickEvent);
6854 });
6855
6856 return featureInterface;
6857});
6858
6859// = == =============================== end: FeatureInterface ================================ == =
6860
6861
6862// = == =============================== External publicInterface ================================ == =
6863
6864webTranslator.features.define('publicInterface', ['webTranslatorCore', 'eventHandling', 'domElements'], (webTranslatorCore, {ON, on}, domElements) => {
6865
6866 function getLangInfo(){
6867 return {
6868 sourceLang: (webTranslatorCore.sourceLang()||"").toLowerCase(),
6869 selectedSourceLang: webTranslatorCore.getSelectedSourceLang().toLowerCase(),
6870 targetLang: (webTranslatorCore.targetLang() || webTranslatorCore.getSelectedTargetLang()).toLowerCase()
6871 };
6872 }
6873 const publicInterface = {
6874 onSourceTextEmpty: U.createMultiProcedure(),
6875 onSourceTextChanged: U.createMultiProcedure(),
6876 onTranslationReady: U.createMultiProcedure(),
6877 onLangChanged: U.createMultiProcedure(),
6878 onUserEvent: U.createMultiProcedure(),
6879 hasPendingRequests: webTranslatorCore.hasPendingRequests,
6880 getLangInfo: getLangInfo,
6881 getSourceLang: function(){
6882 return getLangInfo().sourceLang;
6883 },
6884 getSelectedSourceLang: function(){
6885 return getLangInfo().selectedSourceLang;
6886 },
6887 setSelectedSourceLang: function(newSourceLang){
6888 var l = newSourceLang.toUpperCase();
6889 webTranslatorCore.selectSourceLang(l==='AUTO' ? 'auto': l);
6890 },
6891 setSourceLang: function(newSourceLang){
6892 console.warn("Deprecated! Please use setSelectedSourceLang(...); then pushPreferredLang(...); instead");
6893 publicInterface.selectSourceLang(newSourceLang);
6894 },
6895 getTargetLang: function(){
6896 return getLangInfo().targetLang;
6897 },
6898 setTargetLang: function(newTargetLang){
6899 webTranslatorCore.setSelectedTargetLang(newTargetLang.toUpperCase());
6900 },
6901 getTargetText: function(){ return $targetEdit.val(); },
6902 getSourceText: function(){ return $sourceEdit.val(); },
6903 setSourceText: function(text){
6904 webTranslatorCore.getActiveState().clearSentences();
6905 domElements.$targetEdit.val("");
6906 domElements.$translationsAsText.empty();
6907 resetSourceText(new RequestContext(webTranslatorCore.getActiveState(), TRIGGER__SOURCE_TEXT_CHANGED), text);
6908 },
6909 getPreferredLangs: function(){
6910 return webTranslatorCore.getPreferredLangs().map(function(l){return l.toLowerCase();});
6911 },
6912 pushPreferredLang: function(langCode){
6913 webTranslatorCore.pushPreferredLang(langCode.toUpperCase());
6914 }
6915 };
6916 on(ON.ON__TRANSLATION_READY, function(data){
6917 publicInterface.onTranslationReady(data);
6918 });
6919 on(ON.ON__SOURCE_TEXT_EMPTY, function(){
6920 publicInterface.onSourceTextEmpty();
6921 });
6922 on(ON.ON__SOURCE_TEXT_CHANGED, function(_requestContext){
6923 publicInterface.onSourceTextChanged();
6924 });
6925 on(ON.ON__USER_EVENT, publicInterface.onUserEvent);
6926 var langStr = U.value();
6927 U.withValue([webTranslator._selectedSourceLang, webTranslator.sourceLang, webTranslator.targetLang, webTranslator.selectedTargetLang], function(_){
6928 langStr(JSON.stringify(getLangInfo()));
6929 });
6930 langStr.onValueChanged.push(function(){
6931 publicInterface.onLangChanged(getLangInfo());
6932 });
6933 // publicInterface.onTranslationReady.push(function(){console.log(publicInterface.getTargetText());});
6934 webTranslatorCore.publicInterface = publicInterface;
6935
6936 return publicInterface;
6937});
6938
6939// = == =============================== end: External publicInterface ================================ == =
6940
6941
6942},
6943// -----------------------------------------------------------------------------------------------------------------------------------
6944
6945initTargetEditToolbar: function _initTargetEditToolbar(webTranslator){
6946
6947webTranslator.features.define('TargetToolbar', ['featureInterface', 'localizedTexts', 'domElements'],
6948 function(featureInterface, localizedTexts, domElements){
6949
6950 // = == =============== Target Edit Toolbar =============== == =
6951
6952 const TargetToolbar = (function(){
6953 // const TAG = "[TargetToolbar]";
6954
6955 const toolbarNode = H2.createNode( {
6956 type: "lmt_targetToolbar__container",
6957 isVisible: U.computedValue(featureInterface.isSourceTextEmpty, isEmpty => !isEmpty),
6958 });
6959 domElements.targetEditOuterContainer.appendChild(toolbarNode);
6960
6961 // init
6962 setTimeout(() => {
6963 LMT_UI_Tooltip.initTooltips(toolbarNode);
6964 featureInterface.onAnyclick.push(LMT_UI_Tooltip.closeAllTooltips);
6965 });
6966
6967 return {
6968 $toolbar: $(toolbarNode),
6969 toolbarNode: toolbarNode
6970 }
6971 })();
6972
6973 // = == =============== end: Target Edit Toolbar =============== == =
6974
6975
6976 // = == =============== Copy feature =============== == =
6977 {
6978 // ui
6979 const buttonNode = H2.createNode( {
6980 type: "lmt_targetToolbar__copyButton",
6981 doCopy: () => {
6982 LMT_UI_Utils.copyTextFromInputElement(domElements.$targetEdit).then( () => {
6983 LMT_UI_FeedbackMessage.show({
6984 target: $(buttonNode),
6985 content: localizedTexts.get('translator/copyText.feedback', "Text has been copied"),
6986 timeout: 1000
6987 });
6988 }, err => {
6989 console.warn("Could no write to clipboard.", err);
6990 });
6991 }
6992 });
6993 TargetToolbar.toolbarNode.appendChild(buttonNode);
6994 }
6995 // = == =============== end: Copy feature =============== == =
6996
6997 return TargetToolbar;
6998});
6999
7000
7001// = == =============== TargetToolbar_Share Button feature =============== == =
7002webTranslator.features.define("TargetToolbar_shareButton", ['TargetToolbar', 'webTranslatorCore', 'localizedTexts', 'eventHandling', 'share'],
7003 function _Feature_TargetToolbar_shareButton(TargetToolbar, webTranslatorCore, localizedTexts, eventHandling, share) {
7004
7005 const dbg = false;
7006 const TAG = "[TargetToolbar_shareButton]";
7007 const closeMenu = U.createCallOnce();
7008
7009 const menuIsOpen = U.value(false);
7010 const buttonNode = H2.createNode( {
7011 type: "lmt_targetToolbar__shareButton",
7012 menuIsOpen,
7013 doToggleMenu: () => {
7014 if (menuIsOpen()){
7015 closeMenu();
7016 }
7017 else if(webTranslator.hasPendingRequests()){
7018 LMT_UI_FeedbackMessage.show({
7019 target: $container,
7020 content: localizedTexts.get('translator/toolbar.waitForRequestsFeedback', "Please wait for all requests to finish."),
7021 timeout: 1000,
7022 class: "lmt__feedback_message--warn"
7023 });
7024 }
7025 else {
7026 doOpenMenu();
7027 }
7028 }
7029 });
7030 const $container = $(buttonNode);
7031 U.withValue(menuIsOpen, isOpen => isOpen || closeMenu());
7032
7033 function doOpenMenu(){
7034 M.requireAsync(['Shariff'], function(Shariff){
7035 const url_shortened_targetText = share.createTextURL(webTranslatorCore.getActiveState(), share.targetTextHasBeenAltered(webTranslatorCore.getActiveState()));
7036 var url = url_shortened_targetText[0];
7037 dbg && console.log(TAG, "Open share menu. URL: ", url, url.length, url_shortened_targetText[1]);
7038 var $menu = $("<div><h2>" + localizedTexts.get('translator/shareMenu.headline', "share") + "</h2><label>" + localizedTexts.get('translator/shareMenu.urlToShare', "URL to share") + "</label>" +
7039 "<input type='text' /><div class='lmt__target_toolbar__copy_link'><button>" + localizedTexts.get('translator/shareMenu.copyUrlButton', "copy URL") + "</button></div></div>");
7040 if (url_shortened_targetText[1]){// text has been shortened
7041 $menu.append("<div class='lmt__target_toolbar__share_shortened_hint'>" + localizedTexts.get('translator/shareMenu.textShortendHint', "Note: Only short texts can be shared directly. Only the first sentences will be included.") + "</div>");
7042 }
7043 $menu.append("<div class='lmt__target_toolbar__share_buttons'></div>")
7044 .appendTo($container);
7045
7046 $menu.find('.lmt__target_toolbar__copy_link button').click(function(){
7047 LMT_UI_Utils.copyTextFromInputElement($menu.find('input'));
7048 });
7049
7050 $menu.find('input')
7051 .on('click', function(){
7052 $(this).focus().get(0).select();
7053 })
7054 .val(url);
7055 new Shariff($menu.find('.lmt__target_toolbar__share_buttons'), {
7056 backendUrl: null,
7057 lang: dlPageState.il.toLowerCase().substr(0,2),
7058 mailBody: url_shortened_targetText[2] + "\n \n " + localizedTexts.get('translator/share.mailBody', "Translated with www.DeepL.com/translator") + "\n {url}",
7059 mailSubject: localizedTexts.get('translator/share.mailSubject', "Translation via DeepL Translator"),
7060 mailUrl: 'mailto:',
7061 title: url_shortened_targetText[2] + "\n", // localizedTexts.get('translator/share.title', "Translated with www.DeepL.com/translator"),
7062 theme: "normal",
7063 url: url,
7064 orientation: 'horizontal',
7065 services: ['twitter', 'facebook', 'mail']
7066 });
7067
7068// setTimeout(function(){ // use timeout or edge will not show the menu until the mouse has been moved
7069// $container.addClass('lmt__target_toolbar__share--open');
7070// });
7071 menuIsOpen(true);
7072 closeMenu.push(() =>{
7073 $menu.remove();
7074 menuIsOpen(false);
7075 //$container.removeClass('lmt__target_toolbar__share--open');
7076 });
7077 LMT_UI_Tooltip.initTooltips($menu);
7078 });
7079 }
7080 TargetToolbar.toolbarNode.appendChild(buttonNode);
7081
7082 // ---- ui
7083
7084 eventHandling.on(eventHandling.ON.ON__ACTIVE_ELEMENT_CHANGED, function($activeElement){
7085 if (!$activeElement.closest(".lmt__target_toolbar__share,.lmt__mobile_share_container").length){
7086 closeMenu();
7087 }
7088 });
7089 eventHandling.on(eventHandling.ON.ON__ANYCLICK, function($targetElement){
7090 $targetElement.closest(".lmt__target_toolbar__share,.lmt__mobile_share_container button,.lmt__mobile_share_container > div").length || closeMenu();
7091 });
7092
7093 // ---- mobile ui
7094
7095 var $mobileContainer = $("<div class='lmt__mobile_share_container lmt__mobile_share_container--inactive'></div>").appendTo($('.lmt__sides_container'));
7096 var $mobileButton = $("<button title='" + localizedTexts.get('translator/share.tooltip', "share translation") + "'></button>").appendTo($mobileContainer);
7097
7098 function checkStatus(){
7099 if (webTranslatorCore.hasPendingRequests() || webTranslatorCore.sourceIsEmpty()){
7100 $mobileContainer.addClass('lmt__mobile_share_container--inactive');
7101 }else{
7102 $mobileContainer.removeClass('lmt__mobile_share_container--inactive');
7103 }
7104 }
7105 webTranslatorCore.hasPendingRequests.onValueChanged.push(checkStatus);
7106 webTranslatorCore.sourceIsEmpty.onValueChanged.push(checkStatus);
7107
7108 $mobileButton.click(function(){
7109 if (navigator.share){
7110 if (webTranslatorCore.hasPendingRequests() || webTranslatorCore.sourceIsEmpty()){
7111 return;
7112 }
7113 log && log(TAG, "Use share api");
7114 var url_shortened_targetText = share.createTextURL(webTranslatorCore.getActiveState(), share.targetTextHasBeenAltered(webTranslatorCore.getActiveState()));
7115
7116 navigator.share({
7117 title: localizedTexts.get('translator/share.mailSubject', "Translation via DeepL Translator"),
7118 text: url_shortened_targetText[2],
7119 url: url_shortened_targetText[0],
7120 });
7121 return;
7122 }
7123
7124 M.requireAsync(['Shariff'], function(Shariff){
7125 if ($mobileContainer.hasClass('lmt__mobile_share_container--open')){
7126 closeMenu();
7127 } else {
7128 if (webTranslatorCore.hasPendingRequests() || webTranslatorCore.sourceIsEmpty()){
7129 return;
7130 }
7131 var url_shortened_targetText = share.createTextURL(webTranslatorCore.getActiveState(), share.targetTextHasBeenAltered(webTranslatorCore.getActiveState()));
7132 var url = url_shortened_targetText[0];
7133 dbg && console.log(TAG, "Open mobile share menu. URL: ", url, url.length, url_shortened_targetText[1]);
7134 var $menu = $("<div><h2>" + localizedTexts.get('translator/shareMenu.headline', "Share") + "</h2></div>");
7135 if (url_shortened_targetText[1]){// text has been shortened
7136 $menu.append("<div class='lmt__target_toolbar__share_shortened_hint'>" + localizedTexts.get('translator/shareMenu.textShortendHint', "Note: Only short texts can be shared directly. Only the first sentences will be included.") + "</div>");
7137 }
7138 var $buttons = $("<div class='lmt__mobile_share_container__share_buttons'></div>").appendTo($menu);
7139 $menu.appendTo($mobileContainer);
7140
7141 new Shariff($buttons, {
7142 backendUrl: null,
7143 lang: dlPageState.il.toLowerCase().substr(0,2),
7144 mailBody: url_shortened_targetText[2] + "\n \n " + localizedTexts.get('translator/share.mailBody', "Translated with www.DeepL.com/translator") + "\n {url}",
7145 mailSubject: localizedTexts.get('translator/share.mailSubject', "Translation via DeepL Translator"),
7146 mailUrl: 'mailto:',
7147 title: url_shortened_targetText[2] + "\n", // localizedTexts.get('translator/share.title', "Translated with www.DeepL.com/translator"),
7148 theme: "normal",
7149 url: url,
7150 orientation: 'horizontal',
7151 services: ['twitter', 'facebook', 'whatsapp', 'mail']
7152 });
7153
7154 setTimeout(function(){ // use timeout or edge will not show the menu until the mouse has been moved
7155 $mobileContainer.addClass('lmt__mobile_share_container--open');
7156 });
7157 closeMenu.push(function(){
7158 log(TAG, "closing menu");
7159 $menu.remove();
7160 $mobileContainer.removeClass('lmt__mobile_share_container--open');
7161 });
7162 }
7163 });
7164 });
7165 $('.lmt__mobile_share_container').click(function(){
7166 if ($mobileContainer.hasClass('lmt__mobile_share_container--open'))
7167 closeMenu();
7168 });
7169});
7170// = == =============== end: TargetToolbar_Share Button feature =============== == =
7171
7172
7173// = == =============== TargetToolbar_Download feature =============== == =
7174/*
7175 https://stackoverflow.com/questions/19327749/javascript-blob-filename-without-link
7176 https://github.com/eligrey/FileSaver.js
7177*/
7178!DeviceProps.isIPad && webTranslator.features.define('TargetToolbar_downloadButton',
7179 ['TargetToolbar', 'webTranslatorCore', 'publicInterface', 'featureInterface', 'localizedTexts', 'eventHandling'],
7180 function _Feature__TargetToolbar_downloadButton(TargetToolbar, webTranslatorCore, publicInterface, featureInterface, localizedTexts, eventHandling){
7181
7182 const TAG = "[FileSave]";
7183 function download(_filename, _format){
7184 return new Promise(function(success, _failure){
7185 M.requireAsync(['FileSaver_saveAs'], function(FileSaver_saveAs){
7186 const format = _format || '.txt';
7187
7188 let filename = _filename;
7189 if (!filename){
7190 filename = "DeepL_" + publicInterface.getSourceLang() + "-" + publicInterface.getTargetLang() + "_"
7191 let s = featureInterface.getFirstTargetSentence();
7192 if (s){
7193 if (s.length>32){
7194 const pos = s.indexOf(" ", 24);
7195 if (pos>0 && pos<=32){
7196 s = s.substr(0, pos) + " …";
7197 }
7198 else {
7199 s = s.substr(0, 32) + "…";
7200 }
7201 }
7202 s = s.trim().replace(/[~"#%&*:<>?/\\{|}.]/g, "_");
7203 s.length && (filename += "(" + s + ")");
7204 }
7205 filename += "." + format;
7206 }
7207 let text;
7208 if (format === 'tmx'){
7209
7210 const entries = [];
7211 featureInterface.getTargetSentences().forEach( tSentence => {
7212 const sSentence = tSentence.getSourceSentence();
7213 entries.push(sSentence.getNormalizedText() + "->\n" + tSentence.getNormalizedText() );
7214 });
7215 text = entries.join("\n");
7216 const sourceLang = publicInterface.getSourceLang();
7217 const targetLang = publicInterface.getTargetLang();
7218 let prevSourceSentenceText, prevTargetSentenceText;
7219 const content = [
7220 {
7221 meta: "xml",
7222 attr: {version:"1.0", encoding:"utf-8"}
7223 },
7224 {
7225 tag: "tmx",
7226 attr: { version: "1.4"},
7227 content: [
7228 {
7229 tag: "header",
7230 attr: {
7231 creationtool: "DeepL Translator",
7232 creationtoolversion: "20190507",
7233 datatype: "plaintext",
7234 segtype: "sentence",
7235 adminlang: "en-GB",
7236 srclang: sourceLang
7237 }
7238 },
7239 {
7240 tag: "body",
7241 content: featureInterface.getTargetSentences().map( (tSentence, i) => {
7242 const sSentence = tSentence.getSourceSentence();
7243 const entry = {
7244 tag: "tu",
7245 attr: {tuid: i},
7246 content: [
7247 prevSourceSentenceText ? {
7248 tag: "prop",
7249 attr: {type: "x-ContextContent"},
7250 content: prevSourceSentenceText.replace(/[|]/g,"||") + " | | " + prevTargetSentenceText.replace(/[|]/g,"||") + " | "
7251 } : false,
7252 {
7253 tag: "tuv",
7254 attr: {"xml:lang": sourceLang},
7255 content: {
7256 tag: "seg",
7257 content: sSentence.getNormalizedText()
7258 }
7259 },
7260 {
7261 tag: "tuv",
7262 attr: {"xml:lang": targetLang},
7263 content: {
7264 tag: "seg",
7265 content: tSentence.getNormalizedText()
7266 }
7267 },
7268 ]
7269 };
7270 prevSourceSentenceText = sSentence.getNormalizedText();
7271 prevTargetSentenceText = tSentence.getNormalizedText();
7272 return entry;
7273 })
7274 }
7275 ]
7276 }
7277 ];
7278 text = LMT_Utils.toXML(content);
7279 } else {
7280 text = publicInterface.getTargetText();
7281 if (text.trim().length && !featureInterface.isPro){
7282 text += "\n\n" + (featureInterface.getCopyFooter(publicInterface.getTargetLang()));
7283 }
7284 if ((navigator.platform||"").toLowerCase().indexOf('win')>=0){
7285 log(TAG, "Use windows line breaks.");
7286 text = text.replace(/\n/g, "\r\n");
7287 }
7288 }
7289 console.log(TAG, "Save text as ", filename);
7290
7291 FileSaver_saveAs(new Blob([text], {type: "text/plain;charset=utf-8"}), filename);
7292 success(filename);
7293 });
7294 });
7295 }
7296 window._download = download;
7297
7298 // ui
7299 (function (){
7300 function executeDownload(format){
7301 download(undefined, format).then(function(_filename){
7302 LMT_UI_FeedbackMessage.show({
7303 target: $(buttonNode),
7304 content: localizedTexts.get('translator/saveText.successFeedback', "Text file created"),
7305 timeout: 1000,
7306 });
7307 }, function(err){console.error(err);});
7308 }
7309
7310
7311 const buttonNode = H2.createNode( {
7312 type: "lmt_targetToolbar__saveButton",
7313 doSave: () => {
7314 if (webTranslatorCore.hasPendingRequests()){
7315 LMT_UI_FeedbackMessage.show({
7316 target: buttonNode,
7317 content: localizedTexts.get('translator/toolbar.waitForRequestsFeedback', "Please wait for all requests to finish"),
7318 timeout: 1000,
7319 class: "lmt__feedback_message--warn"
7320 });
7321 }
7322 else if (dlPageState.experimental || window.location.href.indexOf('beta.deepl.com') !== -1){
7323 const $menu = $("<div class='lmt__target_toolbar__save_popup'></div>");
7324 $('<button>.txt</button>')
7325 .on('click', () => executeDownload('txt'))
7326 .appendTo($menu);
7327 $('<button>.tmx</button>')
7328 .on('click', () => executeDownload('tmx'))
7329 .appendTo($menu);
7330
7331 $menu.appendTo($(buttonNode));
7332 const doClose = U.createCallOnce();
7333 doClose.push(()=>$menu.remove())
7334
7335 setTimeout(() => eventHandling.on(eventHandling.ON.ON__ANYCLICK, $targetElement => {
7336 if (!$targetElement.closest(".lmt__target_toolbar__save_popup").length){
7337 doClose();
7338 return 'REMOVE';
7339 }
7340 }));
7341 }
7342 else {
7343 executeDownload();
7344 }
7345 }
7346 });
7347 TargetToolbar.toolbarNode.appendChild(buttonNode);
7348
7349 //const $container = $("<div title='" + localizedTexts.get('translator/saveText.tooltip', "Save as text file") + "' class='lmt__target_toolbar__save' dl-test='translator-target-toolbar-download'></div>");
7350 //const $button = $("<button></button>").appendTo($container);
7351
7352 //$container.appendTo(TargetToolbar.$toolbar);
7353 })();
7354 return true;
7355});
7356// = == =============== end: TargetToolbar_Download =============== == =
7357
7358// = == ============== Dev Retranslate button feature ============ == =
7359dlPageState.experimental && webTranslator.features.define('TargetToolbar_retranslateButton', ['webTranslatorCore', 'TargetToolbar', 'dev_retranslate'], function _Feature__TargetToolbar_retranslateButton(webTranslatorCore, TargetToolbar, dev_retranslate){
7360 const $container = $("<div title='retranslate' class='lmt__target_toolbar__generic' dl-test='translator-target-toolbar-retranslate'></div>");
7361 $("<button>↻</button>").appendTo($container).click(dev_retranslate);
7362 $container.appendTo(TargetToolbar.$toolbar);
7363
7364});
7365
7366
7367// = == ============== observeFullTextData ============ == =
7368webTranslator.features.defineFeature_lazy('observeFullTextData',
7369 ['config', 'translationBackend', 'webTranslatorCore', 'eventHandling', 'domElements'],
7370 (config, translationBackend, webTranslatorCore, {ON, on}, domElements) => {
7371 const TAG = "[observeFullTextData]";
7372 const dbg = config.get('CONFIG__IS_DEV');
7373 function getLogData() {
7374 const state = webTranslatorCore.getActiveState();
7375 const data = {
7376 type: "sourceText",
7377 lang: {
7378 source: state.getSourceLang(),
7379 target: state.getTargetLang(),
7380 selectedSource: webTranslatorCore.getSelectedSourceLang(),
7381 selectedTarget: webTranslatorCore.getSelectedTargetLang(),
7382 interface: (dlPageState.il || "").toUpperCase(),
7383 },
7384 sourceText: (domElements.sourceEdit.value || "").substr(0, 5000)
7385 };
7386 return data;
7387 }
7388
7389 return {
7390 enable(){
7391 if (!config.get('CONFIG__DATA_USE_ALLOWED') || config.get('CONFIG__IS_PRO')){
7392 console.error(TAG, "invalid"); // sanity check failed!
7393 return false;
7394 }
7395
7396 dbg && console.log(TAG, "Logging!");
7397 const debounceTime = 5000;
7398 const executeLatestWhenDone = U.executeOnlyLatestWhenDoneProcedure();
7399 let lastText = "";
7400
7401 function scheduleLogRequests(){
7402 executeLatestWhenDone(done => {
7403 dbg && console.log(TAG, "start");
7404 setTimeout(() => {
7405 const data = getLogData();
7406 if (lastText === data.sourceText || !data.sourceText.trim()){
7407 dbg && console.log(TAG, "skip");
7408 done();
7409 }
7410 else {
7411 dbg && console.log(TAG, "Send...", data);
7412 translationBackend.sendAdditionalData(data)
7413 .then( () => {
7414 dbg && console.log(TAG, "...done");
7415 lastText = data.sourceText;
7416 done();
7417 }, (error) => {
7418 console.warn(error);
7419 dbg && console.log(TAG, "...failed");
7420 done();
7421 });
7422 }
7423 }, debounceTime);
7424 });
7425 }
7426 on(ON.ON__SOURCE_SENTENCES_CHANGED, _rqContext => scheduleLogRequests());
7427 }
7428 };
7429});
7430// = == ============== end: observeFullTextData ============ == =
7431
7432
7433}, // end: return {...}
7434
7435initNativeAppFeatures: function _initNativeAppFeatures(webTranslator){
7436 webTranslator.features.define('AppPrivacy', ['translationBackend'], function _appPrivacy (translationBackend) {
7437 const doNotSaveValue = U.persistentValue('appPrivacy.doNotSave', false);
7438
7439 U.withValue([doNotSaveValue], (doNotSave) => {
7440 if (doNotSave) {
7441 translationBackend.doFinalizeJobRequestParameters.push(requestParams =>
7442 doNotSaveValue() ? requestParams.jobs.forEach(job => job['do_not_save'] = true) : 'REMOVE');
7443 translationBackend.doFinalizeSentenceSplittingRequestParameters.push(requestParams =>
7444 doNotSaveValue() ? (requestParams['do_not_save'] = true) : 'REMOVE');
7445 }
7446 });
7447
7448 return doNotSaveValue;
7449 });
7450
7451 // add app delay feature for non pro users
7452 webTranslator.defineFeature('DelayNoPro', ['domElements', 'webTranslatorCore', 'translationBackend', 'eventHandling'],
7453 function(domElements, webTranslatorCore, {lmtBackends}, eventHandling){
7454 const translatorClasses = ['lmt__source_textarea', 'lmt__clear_text_button', 'lmt__target_textarea'];
7455 const removeDelayNotification = U.createCallOnce();
7456
7457 const lmtBackend = lmtBackends['translation']()
7458 let delaySimulationIsActive = false
7459
7460 function finishAppBlock () {
7461 translatorClasses.forEach(
7462 (className) => {
7463 Array.from(document.getElementsByClassName(className)).forEach(el => el.disabled = false)
7464 }
7465 )
7466 delete(webTranslatorCore.globalState['GSTATE__ADDITIONAL_JOB_REQUEST_PARAMETERS']['key'])
7467 }
7468
7469 function showDelayNotification (rpcError) {
7470 // this removes any existing notification
7471 removeDelayNotification();
7472
7473 if (rpcError && rpcError.data && rpcError.data.responseData) {
7474 const errorData = rpcError.data.responseData
7475 const seconds = U.value(errorData.timeout);
7476
7477 webTranslatorCore.globalState['GSTATE__ADDITIONAL_JOB_REQUEST_PARAMETERS']['key'] = errorData.key
7478
7479 const message = H2.createNode( {
7480 type: "lmt_app_delay",
7481 placeholders: {seconds: seconds}
7482 });
7483
7484 const proLink = message.querySelector('.lmt__app_delay_no_pro_notification__footer > a')
7485 if (proLink) {
7486 proLink.onclick = ((clickEvent) => {
7487 clickEvent.preventDefault()
7488 dlAnalytics.send({
7489 dryRun: queryVars['dev_block_app'],
7490 category: 'app',
7491 action: 'appDelayGoProClicked',
7492 label: 'windows',
7493 value: 1,
7494 });
7495 eventHandling.notify(eventHandling.ON.ON__USER_EVENT, {
7496 type: "userRequestsProPage",
7497 context: "appDelay"
7498 })
7499 })
7500 }
7501
7502
7503 translatorClasses.forEach(
7504 (className) => {
7505 Array.from(document.getElementsByClassName(className)).forEach(el => el.disabled = true)
7506 }
7507 )
7508 domElements.rootContainer.appendChild(message);
7509
7510 const createTimeout = () => {
7511 setTimeout(() => {
7512 if (seconds() > 1) {
7513 seconds(seconds() - 1);
7514 createTimeout()
7515 }
7516 }, 1000)
7517 }
7518 createTimeout()
7519
7520 removeDelayNotification.push(() => {
7521 domElements.rootContainer.removeChild(message);
7522 })
7523
7524 dlAnalytics.send({
7525 dryRun: queryVars['dev_block_app'],
7526 category: 'app',
7527 action: 'showAppDelayNotification',
7528 label: 'windows',
7529 value: 1,
7530 });
7531 }
7532
7533 lmtBackend.onRequestSucceeded.push( (requestType) => {
7534 if (requestType === 'translation') {
7535 finishAppBlock();
7536 removeDelayNotification();
7537 return 'REMOVE';
7538 }
7539 })
7540
7541 if (queryVars['dev_block_app']) {
7542 const location = window.location.toString().replace(/(?:\?dev_block_app=[^&]+$)|(dev_block_app=[^&]+&)|(&dev_block_app=[^&]+)/, '');
7543 window.history.replaceState( {} , window.title, location );
7544 // delete(lmtBackend.rpcConfig.queryParams['x-simulate-app-usage-timeout'])
7545 }
7546
7547 if (delaySimulationIsActive) {
7548 removeDelaySimulation()
7549 }
7550 }
7551
7552 function enableDelaySimulation (appDelay) {
7553 if (!lmtBackend.rpcConfig) {
7554 lmtBackend.rpcConfig = {}
7555 }
7556
7557 if (!lmtBackend.rpcConfig.queryParams) {
7558 lmtBackend.rpcConfig.queryParams = {};
7559 }
7560 lmtBackend.rpcConfig.queryParams['x-simulate-app-usage-timeout'] = appDelay;
7561 delaySimulationIsActive = true
7562 console.log(`simulating delay for ${appDelay} seconds`)
7563 }
7564
7565 function removeDelaySimulation () {
7566 delete(lmtBackend.rpcConfig.queryParams['x-simulate-app-usage-timeout'])
7567 delaySimulationIsActive = false
7568 }
7569
7570
7571 if (queryVars['dev_block_app']) {
7572 const appDelay = parseFloat(queryVars['dev_block_app'], 10) || 0;
7573 if (appDelay > 0) {
7574 enableDelaySimulation('' + appDelay)
7575 }
7576 }
7577
7578 lmtBackend.onRequestFailed.push( (requestType, rpcError) => {
7579 if (requestType === 'translation' && rpcError.code === 1042910) {
7580 showDelayNotification(rpcError);
7581 }
7582 })
7583 // showDelayNotification();
7584 return {
7585 showDelayNotification: showDelayNotification,
7586 finishAppBlock: finishAppBlock,
7587 removeDelayNotification: removeDelayNotification,
7588 enableDelaySimulation: enableDelaySimulation,
7589 removeDelaySimulation: removeDelaySimulation,
7590 }
7591
7592 }
7593 );
7594
7595 webTranslator.features.define('AppUsageStats', ['eventHandling', 'translationBackend'],
7596 function _Feature__AppUsageStats(eventHandling, translationBackend){
7597 const usageDays = U.persistentValue('appUsageStats.days', 0);
7598 const lastUsageDay = U.persistentValue('appUsageStats.lastDay', false);
7599 lastUsageDay.onValueChanged.push(() => usageDays(usageDays() + 1));
7600 eventHandling.on(eventHandling.ON.ON__TRANSLATION_READY, () => lastUsageDay((new Date).toISOString().substring(0, 10)));
7601
7602 translationBackend.doFinalizeJobRequestParameters.push(requestParams =>
7603 (requestParams['apps'] || (requestParams['apps'] = {}))['usage'] = usageDays());
7604
7605 return {
7606 usageDays,
7607 lastUsageDay
7608 }
7609 }
7610 );
7611
7612 webTranslator.features.define('AppTargetLangMenuSizeFixer', ['eventHandling', 'domElements'],
7613 function _Feature__AppTargetLangMenuSizeFixer(eventHandling, domElements){
7614 eventHandling.on(eventHandling.ON.ON__LANGUAGE_SELECT_OPENED, (selectType, theSelect) => {
7615 if (selectType === 'target') {
7616 const langSelectMenuBottomMargin = 5;
7617 const targetContainerOuterHeight = $(domElements.targetSideContainer).outerHeight();
7618 const targetLangSelectHeight = $(theSelect).height();
7619 const availableHeight = targetContainerOuterHeight - targetLangSelectHeight - langSelectMenuBottomMargin;
7620 $(theSelect).find('.lmt__language_select__menu').css({
7621 'overflow-y': 'auto',
7622 'max-height': availableHeight + 'px'
7623 });
7624 }
7625 });
7626 }
7627 );
7628}
7629}}); // end: define(...)
7630
7631// M.requireAsync(['bar'], function(bar) {
7632// console.log('M.requireAsync', bar)
7633// })
7634
7635// import bar from '../dev/bar'
7636// console.log('import', bar)