8 description:
"Use a network API to translate selected/filtered rows",
9 shortcut:
"Ctrl+Shift+T",
10 wbModules: {
"LocalizationEditor" },
11 awesomeFontCode: 0xF1AB)]
12class TranslationPlugin : LocalizationEditorPlugin
19 protected bool m_bDisableLocalesGrouping;
25 [
Attribute(defvalue: ETranslationPlugin_EditedRowsMode.PROCESS_ALL.ToString(), uiwidget:
UIWidgets.ComboBox,
desc:
"Define the behaviour of edited entries (among the selected ones)", enumType: ETranslationPlugin_EditedRowsMode,
category:
"Selection")]
26 protected ETranslationPlugin_EditedRowsMode m_eEditedEntriesMode;
32 [
Attribute(defvalue:
"0",
desc:
"Overwrite existing translations if the entry itself is eligible to translation; otherwise only fill empty fields",
category:
"Actions")]
33 protected bool m_bOverwriteExistingTranslations;
35 [
Attribute(defvalue:
"1",
desc:
"Set entry's edited value as original value"
36 +
"\ne.g Target_en_us takes the value of Target_en_us_edited and Target_en_us_edited gets cleared",
38 protected bool m_bSetEditedAsOriginal;
44 [
Attribute(
desc:
"[Mandatory] The translation server API URL, e.g http:/" +
"/myserver/?mode=translate\nDefault protocol: " + DEFAULT_PROTOCOL +
":/" +
"/",
category:
"Advanced")]
45 protected string m_sServerURL;
48 protected string m_sServerToken;
50 [
Attribute(defvalue:
"0",
desc:
"Show which generative AI model was used, cost etc (if available)",
category:
"Advanced")]
51 protected bool m_bDisplayAdvancedStatsOnSuccess;
53 [
Attribute(
desc:
"[Mandatory] Locale-matching and special fields listing (see it as a string table item adapter)",
category:
"Advanced")]
54 protected ref TranslationPluginMatchConfig m_MatchConfig;
61 protected bool m_bLogChanges;
64 protected bool m_bLogNetwork;
66 [
Attribute(defvalue: ETranslationPlugin_ProcessMode.PRODUCTION.ToString(), uiwidget:
UIWidgets.ComboBox, enumType: ETranslationPlugin_ProcessMode,
category:
"Debug")]
67 protected ETranslationPlugin_ProcessMode m_eProcessMode;
71 protected int m_iTranslationQueriesCount;
72 protected ref array<string> m_aGlobalLocales;
75 protected bool m_bWaitingOnRestAPI;
76 protected int m_iLastUsage;
78 protected ref WBProgressDialog m_ProgressBar;
80 protected static const string PLUGIN_NAME =
"Translation plugin";
82 protected static const string SOURCE_ID =
"SOURCE";
83 protected static const string SOURCE_EDITED_ID =
"SOURCE_EDITED";
85 protected static const int MAX_DISPLAYED_IDS = 6;
88 protected static const int GENDER_MALE_INDEX = 1;
89 protected static const int GENDER_FEMALE_INDEX = 2;
90 protected static const int GENDER_OTHER_INDEX = 3;
92 protected static const string GENDER_MALE_VALUE =
"M";
93 protected static const string GENDER_FEMALE_VALUE =
"F";
94 protected static const string GENDER_OTHER_VALUE =
"O";
96 protected static const string PROTOCOL_SEPARATOR =
":/" +
"/";
97 protected static const string DEFAULT_PROTOCOL =
"https";
98 protected static const ref array<string> ACCEPTED_PROTOCOLS = {
"http" + PROTOCOL_SEPARATOR, DEFAULT_PROTOCOL + PROTOCOL_SEPARATOR };
101 protected static const string TOKEN_WHITELIST =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~+/=";
104 protected override void Run()
109 LocalizationEditor stringEditor = Workbench.GetModule(LocalizationEditor);
112 PrintDialog(
"LocalizationEditor is not available", PLUGIN_NAME,
LogLevel.ERROR);
119 PrintDialog(
"No file opened.", PLUGIN_NAME,
LogLevel.WARNING);
124 if (!stringTableItems)
126 PrintDialog(
"Error within the opened file - no Items property found.", PLUGIN_NAME,
LogLevel.ERROR);
130 int stringTableItemsCount = stringTableItems.Count();
131 if (stringTableItemsCount < 1)
133 PrintDialog(
"The current file is empty.", PLUGIN_NAME,
LogLevel.NORMAL);
138 if (m_bWaitingOnRestAPI)
140 PrintDialog(
"Please wait for the result", PLUGIN_NAME,
LogLevel.NORMAL);
144 array<int> selectedRows = {};
145 stringEditor.GetSelectedRows(selectedRows);
146 int potentialRowsCount = selectedRows.Count();
148 if (potentialRowsCount < 1)
150 stringEditor.GetFilteredRows(selectedRows);
151 potentialRowsCount = selectedRows.Count();
152 if (potentialRowsCount < 1 || potentialRowsCount >= stringTableItemsCount)
154 PrintDialog(
"No lines selected/filtered.", PLUGIN_NAME,
LogLevel.NORMAL);
159 if (!CheckFields(stringTableItems.Get(selectedRows[0])))
162 int start =
System.GetTickCount();
166 int queriesCount = idWrapperMap.Count();
167 if (queriesCount < 1)
169 PrintDialog(
"No selected rows need translation following the set options.", PLUGIN_NAME,
LogLevel.NORMAL);
174 if (queriesCount == 1)
176 string id = idWrapperMap.GetKey(0);
180 if (potentialRowsCount == 1)
181 message =
"You are about to translate the \"" +
id +
"\" row. Continue?";
183 message =
"You are about to only translate the \"" +
id +
"\" row out of " + potentialRowsCount +
" rows. Continue?";
187 if (queriesCount == potentialRowsCount)
188 message =
string.Format(
"You are about to translate the following %1 rows:\n", queriesCount);
190 message =
string.Format(
"You are about to translate the following %1 out of %2 rows:\n", queriesCount, potentialRowsCount);
193 foreach (
string id, TranslationPlugin_DataWrapper wrapper : idWrapperMap)
196 if (count >= MAX_DISPLAYED_IDS)
198 message +=
"\n\t- (...)";
203 message +=
"\n\t- Unknown";
205 message +=
"\n\t- " +
id;
208 message +=
"\n\nContinue?";
211 message +=
"\n\nOptions:";
212 if (m_bOverwriteExistingTranslations)
213 message +=
"\n✔ Overwrite existing translations";
215 message +=
"\n✖ Overwrite existing translations";
217 if (m_bSetEditedAsOriginal)
219 if (m_MatchConfig.m_sDefaultSourceEdited.IsEmpty())
220 message +=
"\n✖ Set edited translation as translation source (Default Source Edited field is empty)";
222 message +=
"\n✔ Set edited translation as translation source";
226 message +=
"\n✖ Set edited translation as translation source";
229 string processMode =
typename.EnumToString(ETranslationPlugin_ProcessMode, m_eProcessMode);
230 if (Workbench.ScriptDialog(
string.Format(
"[%1] %2", processMode, PLUGIN_NAME), message,
new WorkbenchDialog_OKCancel()) == 0)
233 m_iLastUsage =
System.GetTickCount();
234 m_iTranslationQueriesCount = queriesCount;
236 StartProcess(idWrapperMap);
245 array<string> wrongFields = {};
247 if (!item.Get(m_MatchConfig.m_sIdField, tmp))
248 wrongFields.Insert(
"Id Field (" + m_MatchConfig.m_sIdField +
")");
250 if (!item.Get(m_MatchConfig.m_sDefaultSource, tmp))
251 wrongFields.Insert(
"Default Source (" + m_MatchConfig.m_sDefaultSource +
")");
253 if (m_bSetEditedAsOriginal && !m_MatchConfig.m_sDefaultSourceEdited.IsEmpty() && !item.Get(m_MatchConfig.m_sDefaultSourceEdited, tmp))
254 wrongFields.Insert(
"Default Source Edited (" + m_MatchConfig.m_sDefaultSourceEdited +
")");
256 if (!wrongFields.IsEmpty())
258 string message =
"Wrong fields have been detected in configuration (bad config used?)";
259 foreach (
string wrongField : wrongFields)
261 message +=
"\n- Cannot find " + wrongField;
264 PrintDialog(message, PLUGIN_NAME,
LogLevel.WARNING);
277 int count = idWrapperMap.Count();
291 Print(
"Cannot obtain REST API with game.GetRestApi()",
LogLevel.ERROR);
295 bool areLocalesSame = !m_bDisableLocalesGrouping;
297 TranslationPluginRequest request =
new TranslationPluginRequest();
298 request.locales = {};
299 request.locales.Copy(idWrapperMap.GetElement(0).m_Query.locales);
300 request.locales.Sort();
301 foreach (
string id, TranslationPlugin_DataWrapper dataWrapper : idWrapperMap)
303 request.queries.Insert(dataWrapper.m_Query);
304 dataWrapper.m_Query.locales.Sort();
305 if (count > 1 && areLocalesSame && !AreLocalesEqual(request.locales, dataWrapper.m_Query.locales))
306 areLocalesSame =
false;
311 m_aGlobalLocales = request.locales;
312 foreach (
string id, TranslationPlugin_DataWrapper dataWrapper : idWrapperMap)
314 dataWrapper.m_Query.locales = null;
319 request.locales = null;
322 m_mIdDataMap = idWrapperMap;
323 if (m_eProcessMode == ETranslationPlugin_ProcessMode.PRODUCTION)
325 bool result = SendRequest(restAPI, request);
332 m_bWaitingOnRestAPI =
true;
334 switch (m_eProcessMode)
336 case ETranslationPlugin_ProcessMode.SIMULATE_SUCCESS: ProcessSuccessResponse(GetFakeResponse());
return true;
338 case ETranslationPlugin_ProcessMode.SIMULATE_ERROR_403: ProcessErrorResponse(
HttpCode.HTTP_CODE_403,
ERestResult.EREST_ERROR);
break;
339 case ETranslationPlugin_ProcessMode.SIMULATE_ERROR_404: ProcessErrorResponse(
HttpCode.HTTP_CODE_404,
ERestResult.EREST_ERROR);
break;
340 case ETranslationPlugin_ProcessMode.SIMULATE_ERROR_408: ProcessErrorResponse(
HttpCode.HTTP_CODE_408,
ERestResult.EREST_ERROR_TIMEOUT);
break;
341 case ETranslationPlugin_ProcessMode.SIMULATE_ERROR_418: ProcessErrorResponse(
HttpCode.HTTP_CODE_418,
ERestResult.EREST_ERROR_UNKNOWN);
break;
342 case ETranslationPlugin_ProcessMode.SIMULATE_ERROR_500: ProcessErrorResponse(
HttpCode.HTTP_CODE_500,
ERestResult.EREST_ERROR_SERVERERROR);
break;
343 case ETranslationPlugin_ProcessMode.SIMULATE_ERROR_501: ProcessErrorResponse(
HttpCode.HTTP_CODE_501,
ERestResult.EREST_ERROR_NOTIMPLEMENTED);
break;
350 protected bool SendRequest(notnull
RestApi restAPI, notnull TranslationPluginRequest request)
352 string protocol, address, query;
353 if (!SplitURL(m_sServerURL, protocol, address, query))
355 Print(
"URL \"" + m_sServerURL +
"\" is invalid.",
LogLevel.ERROR);
359 string protocolAndAddress = protocol + address;
362 m_RestCallback.SetOnSuccess(REST_OnSuccess);
363 m_RestCallback.SetOnError(REST_OnError);
365 RestContext restContext = restAPI.GetContext(protocolAndAddress);
368 Print(
"Cannot obtain REST context for " + protocolAndAddress,
LogLevel.ERROR);
372 if (!m_sServerToken.IsEmpty())
374 string sanitisedToken = SanitizeToken(m_sServerToken);
375 if (!sanitisedToken.IsEmpty())
376 restContext.SetHeaders(
"Authorization,Bearer " + sanitisedToken +
",Content-Type,application/json");
379 restContext.SetTimeout(300);
382 string json = request.AsString();
387 PrintFormat(
"Sending JSON by POST to %1, no query\nJSON: %2", protocolAndAddress, json, level:
LogLevel.NORMAL);
389 PrintFormat(
"Sending JSON by POST to %1, query %2\nJSON: %3", protocolAndAddress, query, json, level:
LogLevel.NORMAL);
392 int queriesCount = request.queries.Count();
396 localesCount = request.locales.Count() * queriesCount;
400 foreach (TranslationPluginQuery translationQuery : request.queries)
402 localesCount += translationQuery.locales.Count();
406 m_ProgressBar =
new WBProgressDialog(
string.Format(
"Fetching %1 translation(s) for %2 row(s)...", localesCount, queriesCount), Workbench.GetModule(LocalizationEditor));
407 m_ProgressBar.SetProgress(0.42);
409 restContext.POST(m_RestCallback, query, json);
410 m_bWaitingOnRestAPI =
true;
422 PrintDialog(
"No REST callback returned!", PLUGIN_NAME,
LogLevel.ERROR);
426 if (cb != m_RestCallback)
429 TranslationPluginResponse response;
430 string json = cb.GetData();
434 response =
new TranslationPluginResponse();
435 response.ExpandFromRAW(json);
437 ProcessSuccessResponse(response);
443 protected void ProcessSuccessResponse(notnull TranslationPluginResponse response)
445 LocalizationEditor stringEditor = Workbench.GetModule(LocalizationEditor);
448 PrintDialog(
"LocalizationEditor is not available", PLUGIN_NAME,
LogLevel.ERROR);
453 int start =
System.GetTickCount();
454 int modifiedRows = ApplyTranslations(stringEditor, response);
457 if (modifiedRows > 0)
458 stringEditor.RefreshUI();
460 string message =
string.Format(
461 "Modified %1 out of %2 selected/filtered lines - processing time %3",
463 m_iTranslationQueriesCount,
464 FormatDurationMs(
System.GetTickCount(m_iLastUsage)));
466 if (m_bDisplayAdvancedStatsOnSuccess && response.meta)
469 if (!response.meta.model.IsEmpty())
470 metaMessage +=
"\nAI model: " + response.meta.model;
472 if (response.meta.usage)
474 if (response.meta.usage.prompt_tokens > 0 && response.meta.usage.completion_tokens > 0 && response.meta.usage.total_tokens > 0)
475 metaMessage +=
string.Format(
476 "\nTokens used: %1 (prompt) + %2 (completion) = %3 total",
477 response.meta.usage.prompt_tokens,
478 response.meta.usage.completion_tokens,
479 response.meta.usage.total_tokens);
481 if (response.meta.usage.total_tokens > 0)
482 metaMessage +=
"\nTokens used: " + response.meta.usage.total_tokens;
485 if (response.meta.total_cost > 0)
486 metaMessage +=
"\nTotal cost: " + response.meta.total_cost + response.meta.currency;
488 if (!metaMessage.IsEmpty())
489 message +=
"\n" + metaMessage;
494 PrintDialog(message, PLUGIN_NAME,
LogLevel.NORMAL);
504 PrintDialog(
"No REST callback returned!", PLUGIN_NAME,
LogLevel.ERROR);
508 if (cb != m_RestCallback)
511 ProcessErrorResponse(cb.GetHttpCode(), cb.GetRestResult());
522 string errorCodeStr =
typename.EnumToString(
HttpCode, errorCode);
523 int httpCode = errorCodeStr.ToInt(offset: 10);
528 case 301: errorName =
"Moved Permanently";
break;
529 case 400: errorName =
"Bad Request";
break;
530 case 401: errorName =
"Unauthorized";
break;
531 case 403: errorName =
"Forbidden";
break;
532 case 403: errorName =
"Forbidden";
break;
533 case 404: errorName =
"Not Found";
break;
534 case 408: errorName =
"Request Timeout";
break;
535 case 410: errorName =
"Gone";
break;
536 case 418: errorName =
"I'm a teapot";
break;
537 case 500: errorName =
"Internal Server Error";
break;
538 case 501: errorName =
"Not Implemented";
break;
539 case 502: errorName =
"Bad Gateway";
break;
540 case 503: errorName =
"Service Unavailable";
break;
541 case 504: errorName =
"Gateway Timeout";
break;
542 case 511: errorName =
"Network Authentication Required";
break;
543 default: errorName =
"Unknown - timeout/wrong target?";
break;
546 string message =
string.Format(
547 "Error communicating with the server:\n\nURL: %1\nError: %2 %3\n\n(%4 %5)",
550 errorCodeStr,
typename.EnumToString(
ERestResult, restResult));
552 PrintDialog(message, PLUGIN_NAME +
" - " + errorName,
LogLevel.ERROR);
563 foreach (
int rowToTranslate : rowsToTranslate)
570 if (!item.Get(m_MatchConfig.m_sIdField,
id))
572 PrintFormatDialog(
"Wrong ID field: \"%1\" is invalid", m_MatchConfig.m_sIdField, caption: PLUGIN_NAME, level:
LogLevel.WARNING);
576 if (!item.Get(m_MatchConfig.m_sIdField,
id) ||
id.IsEmpty())
578 Print(
"Element at row " + rowToTranslate +
" does not have an ID",
LogLevel.WARNING);
582 if (!item.GetClassName().ToType() || !item.GetClassName().ToType().IsInherited(
ScriptStringTableItem))
584 Print(
"Element " +
id +
" is not ScriptStringTableItem",
LogLevel.WARNING);
588 TranslationPlugin_DataWrapper dataWrapper = CreateDataWrapper(
id, item);
592 if (result.Contains(
id))
594 Print(
"ID " +
id +
" duplicate detected - skipping",
LogLevel.WARNING);
598 result.Insert(
id, dataWrapper);
608 protected TranslationPlugin_DataWrapper CreateDataWrapper(
string id, notnull
BaseContainer item)
614 item.Get(m_MatchConfig.m_sSkipField, skip);
623 bool isUpdate =
true;
624 if (translations.Find(SOURCE_EDITED_ID, text) && !text.IsEmpty())
626 if (m_eEditedEntriesMode == ETranslationPlugin_EditedRowsMode.PROCESS_UNEDITED_ONLY)
631 if (m_eEditedEntriesMode == ETranslationPlugin_EditedRowsMode.PROCESS_EDITED_ONLY)
635 if (!translations.Find(SOURCE_ID, text) || text.IsEmpty())
639 array<string> locales = {};
640 foreach (
string key,
string value : translations)
642 if (key == SOURCE_ID || key == SOURCE_EDITED_ID)
645 if (isUpdate || m_bOverwriteExistingTranslations || value.IsEmpty())
649 if (locales.IsEmpty())
652 TranslationPlugin_DataWrapper result =
new TranslationPlugin_DataWrapper();
653 result.m_bIsUpdate = isUpdate;
654 result.m_Item = item;
656 result.m_Query =
new TranslationPluginQuery();
657 result.m_Query.id =
id;
658 result.m_Query.text = text;
660 result.m_Query.locales.InsertAll(locales);
662 TranslationRequestMeta meta =
new TranslationRequestMeta();
665 if (item.Get(m_MatchConfig.m_sGenderField, gender))
667 if (gender == GENDER_MALE_INDEX)
668 meta.gender = GENDER_MALE_VALUE;
670 if (gender == GENDER_FEMALE_INDEX)
671 meta.gender = GENDER_FEMALE_VALUE;
673 if (gender == GENDER_OTHER_INDEX)
674 meta.gender = GENDER_OTHER_VALUE;
677 item.Get(m_MatchConfig.m_sCommentField, meta.comment);
678 item.Get(m_MatchConfig.m_sMaxLengthField, meta.maxLength);
681 result.m_Query.meta = meta;
691 string prefix = m_MatchConfig.m_sLocaleFieldsPrefix;
692 string suffix = m_MatchConfig.m_sLocaleFieldsSuffix;
694 int prefixLength = prefix.Length();
695 int suffixLength = suffix.Length();
698 for (
int i, count = item.GetNumVars(); i < count; ++i)
700 string varName = item.GetVarName(i);
702 if (!item.Get(varName, translation))
707 if (varName == m_MatchConfig.m_sDefaultSource)
709 result.Insert(SOURCE_ID, translation);
713 if (varName == m_MatchConfig.m_sDefaultSourceEdited)
715 result.Insert(SOURCE_EDITED_ID, translation);
720 if (prefixLength > 0 && varName != prefix && varName.StartsWith(prefix))
721 locale = varName.Substring(prefixLength, varName.Length() - prefixLength);
723 if (suffixLength > 0 && varName != suffix && varName.EndsWith(prefix))
724 locale = varName.Substring(0, varName.Length() - suffixLength);
726 if (locale.IsEmpty())
731 if (localeMatch.m_sFileLocale == locale)
733 locale = localeMatch.m_sServerLocale;
738 result.Insert(locale, translation);
749 protected int ApplyTranslations(notnull LocalizationEditor stringEditor, notnull TranslationPluginResponse response)
752 bool hasBegunModifying;
754 string prefix = m_MatchConfig.m_sLocaleFieldsPrefix;
755 string suffix = m_MatchConfig.m_sLocaleFieldsSuffix;
757 foreach (TranslationPluginResult result : response.results)
760 TranslationPlugin_DataWrapper wrapper = m_mIdDataMap.Get(result.id);
769 array<string> languagesInResponse = {};
771 int modifiedLanguages;
772 array<string> localesToUse;
773 if (m_aGlobalLocales)
774 localesToUse = m_aGlobalLocales;
776 localesToUse = wrapper.m_Query.locales;
778 foreach (
string locale : localesToUse)
780 string translation = GetTranslationFromLocale(locale, result.translations);
781 int length = translation.Length();
786 PrintFormat(
"Asked for %1's %2 translation, did not obtain it", wrapper.m_Query.id, locale, level:
LogLevel.WARNING);
791 if (wrapper.m_Query.meta && wrapper.m_Query.meta.maxLength > 0 && length > wrapper.m_Query.meta.maxLength)
794 PrintFormat(
"%1's %2 translation is too long: %3 chars > max %4 chars", result.id, locale, length, wrapper.m_Query.meta.maxLength, level:
LogLevel.WARNING);
797 if (!hasBegunModifying)
799 stringEditor.BeginModify(PLUGIN_NAME);
800 hasBegunModifying =
true;
803 stringEditor.ModifyProperty(wrapper.m_Item, wrapper.m_Item.GetVarIndex(prefix + locale + suffix), translation);
810 if (m_bSetEditedAsOriginal && wrapper.m_bIsUpdate && !m_MatchConfig.m_sDefaultSourceEdited.IsEmpty())
812 if (!hasBegunModifying)
814 stringEditor.BeginModify(PLUGIN_NAME);
815 hasBegunModifying =
true;
818 string updatedTranslation;
819 if (wrapper.m_Item.Get(m_MatchConfig.m_sDefaultSourceEdited, updatedTranslation) && !updatedTranslation.IsEmpty())
821 stringEditor.ModifyProperty(wrapper.m_Item, wrapper.m_Item.GetVarIndex(m_MatchConfig.m_sDefaultSource), updatedTranslation);
822 stringEditor.ModifyProperty(wrapper.m_Item, wrapper.m_Item.GetVarIndex(m_MatchConfig.m_sDefaultSourceEdited),
"");
826 "Updating %1's %2 locale to %3 locale's value",
828 m_MatchConfig.m_sDefaultSource,
829 m_MatchConfig.m_sDefaultSourceEdited,
836 if (modifiedLanguages > 0)
840 if (hasBegunModifying)
841 stringEditor.EndModify();
851 protected static string GetTranslationFromLocale(
string locale, notnull TranslationPluginResultHolder holder)
854 if (resource && resource.IsValid())
856 BaseContainer baseContainer = resource.GetResource().ToBaseContainer();
860 if (baseContainer.Get(locale, result))
870 protected void ResetRequestState()
873 m_iTranslationQueriesCount = 0;
874 m_aGlobalLocales = null;
876 m_bWaitingOnRestAPI =
false;
878 m_RestCallback = null;
879 m_ProgressBar = null;
885 protected TranslationPluginResponse GetFakeResponse()
887 TranslationPluginResponse response =
new TranslationPluginResponse();
888 response.results = {};
890 foreach (
string id, TranslationPlugin_DataWrapper wrapper : m_mIdDataMap)
892 TranslationPluginResultHolder translations =
new TranslationPluginResultHolder();
894 if (!resource || !resource.IsValid())
897 BaseContainer container = resource.GetResource().ToBaseContainer();
901 for (
int i, count = container.GetNumVars(); i < count; ++i)
903 string varName = container.GetVarName(i);
904 string varNameUpper = varName;
905 varNameUpper.ToUpper();
906 container.Set(varName, varNameUpper +
" translation");
911 TranslationPluginResult result =
new TranslationPluginResult();
913 result.translations = translations;
915 response.results.Insert(result);
940 protected static bool SplitURL(
string url, out
string protocol, out
string address, out
string query)
946 int protocolSeparatorIndex = url.IndexOf(PROTOCOL_SEPARATOR);
947 if (protocolSeparatorIndex < 0)
949 url = DEFAULT_PROTOCOL + PROTOCOL_SEPARATOR + url;
950 protocolSeparatorIndex = 5;
953 if (url.StartsWith(PROTOCOL_SEPARATOR))
955 url = DEFAULT_PROTOCOL + url;
956 protocolSeparatorIndex = 5;
959 bool isProtocolValid;
960 foreach (
string acceptedProtocol : ACCEPTED_PROTOCOLS)
962 if (url.StartsWith(acceptedProtocol))
964 isProtocolValid =
true;
969 if (!isProtocolValid)
972 int addressIndex = protocolSeparatorIndex + PROTOCOL_SEPARATOR.Length();
973 int urlLength = url.Length();
975 if (urlLength == addressIndex)
978 int qIndex = url.IndexOfFrom(addressIndex,
"?");
979 if (qIndex > -1 && qIndex < addressIndex)
982 protocol = url.Substring(0, addressIndex);
986 address = url.Substring(addressIndex, urlLength - addressIndex);
991 address = url.Substring(addressIndex, qIndex - addressIndex);
992 if (qIndex + 1 == urlLength)
995 query = url.Substring(qIndex + 1, urlLength - qIndex - 1);
1005 static string SanitizeToken(
string token)
1008 for (
int i, length = token.Length(); i < length; ++i)
1010 if (TOKEN_WHITELIST.Contains(token[i]))
1021 protected static bool AreLocalesEqual(notnull array<string> localesA, notnull array<string> localesB)
1023 if (localesA.Count() != localesB.Count())
1026 foreach (
int i,
string localeA : localesA)
1028 if (localeA != localesB[i])
1039 typename configTypeName = TranslationPluginMatchConfig;
1041 BaseContainer baseContainer = resource.GetResource().ToBaseContainer();
1043 m_MatchConfig = TranslationPluginMatchConfig.Cast(managed);
1053 PrintDialog(
"Please provide the Config File Path field in order to load something.", PLUGIN_NAME,
LogLevel.WARNING);
1058 if (!resource.IsValid())
1060 PrintDialog(
"The provided config is not a valid resource.", PLUGIN_NAME,
LogLevel.WARNING);
1064 BaseContainer baseContainer = resource.GetResource().ToBaseContainer();
1067 PrintDialog(
"The provided config does not have a base container.", PLUGIN_NAME,
LogLevel.WARNING);
1074 PrintDialog(
"The provided config cannot be instanciated.", PLUGIN_NAME,
LogLevel.WARNING);
1078 TranslationPluginMatchConfig result = TranslationPluginMatchConfig.Cast(managed);
1081 PrintDialog(
"The provided config is not a valid TranslationPluginMatchConfig.", PLUGIN_NAME,
LogLevel.WARNING);
1085 m_MatchConfig = result;
1094 if (Workbench.ScriptDialog(
1096 "Set the file parsing and communication options here."
1097 +
"\nMandatory fields are Server URL, Match Config, and in Match Config: Id and Default Source.",
1104 protected bool ConfigureClose()
1109 string sanitisedToken = SanitizeToken(m_sServerToken);
1110 if (m_sServerToken != sanitisedToken)
1112 PrintDialog(
"The token you provided was sanitised.", PLUGIN_NAME,
LogLevel.WARNING);
1113 m_sServerToken = sanitisedToken;
1121 protected bool ConfigureValidate()
1123 array<string> errors = {};
1125 m_sServerURL.TrimInPlace();
1126 if (m_sServerURL.IsEmpty())
1127 errors.Insert(
"the Server URL field is empty");
1129 if (m_sServerToken != SanitizeToken(m_sServerToken))
1130 errors.Insert(
"the Server Token value is invalid; be sure to remove any invalid characters (allowed: a-zA-Z0-9-._~+/=)");
1134 m_MatchConfig.m_sIdField.TrimInPlace();
1135 if (m_MatchConfig.m_sIdField.IsEmpty())
1136 errors.Insert(
"the Id field is empty");
1138 m_MatchConfig.m_sDefaultSource.TrimInPlace();
1139 if (m_MatchConfig.m_sDefaultSource.IsEmpty())
1140 errors.Insert(
"the Default Source field is empty");
1144 errors.Insert(
"the Match Config field is null");
1148 if (!errors.IsEmpty())
1150 string message =
"Mandatory fields are missing:";
1151 foreach (
string error : errors)
1153 message +=
"\n- " +
error;
1156 PrintDialog(message, PLUGIN_NAME +
" configuration",
LogLevel.WARNING);
1160 PrintDialog(
"Configuration seems correct.", PLUGIN_NAME +
" configuration",
LogLevel.NORMAL);
1166 protected bool ConfigureLoadConfig()
1168 TranslationPlugin_LoadConfigUI loadConfigUI =
new TranslationPlugin_LoadConfigUI();
1171 if (Workbench.ScriptDialog(PLUGIN_NAME,
"Load a string table match config", loadConfigUI) == 0)
1174 if (loadConfigUI.m_sConfigFilePath.IsEmpty())
1175 PrintDialog(
"Please provide a Config file path (or press Cancel to abort)", PLUGIN_NAME,
LogLevel.WARNING);
1177 if (LoadConfig(loadConfigUI.m_sConfigFilePath))
1186 protected int ButtonLoadConfig()
1188 ConfigureLoadConfig();
1194 protected int ButtonValidateClose()
1196 if (ConfigureValidate())
1204 protected int ButtonClose()
1219 protected static void PrintDialog(
string message,
string caption,
LogLevel level)
1222 Workbench.Dialog(caption, message);
1233 protected static void PrintFormatDialog(
string message,
string param1,
string param2 =
"",
string param3 =
"",
string caption =
"",
LogLevel level =
LogLevel.WARNING)
1236 PrintFormat(
"[%1] %2", caption, message, level: level);
1237 Workbench.Dialog(caption, message);
1244 protected static string FormatDurationMs(
int milliSeconds)
1246 if (milliSeconds == 0)
1249 if (milliSeconds < 0)
1250 milliSeconds = -milliSeconds;
1252 if (milliSeconds < 60000)
1253 return string.Format(
"%1s", (milliSeconds * 0.001).
ToString(lenDec: 1));
1255 int totalSeconds =
Math.Round(milliSeconds * 0.001);
1257 int hours = totalSeconds / 3600;
1258 int minutes = (totalSeconds - hours * 3600) / 60;
1259 int seconds = (totalSeconds - hours * 3600 - minutes * 60);
1262 return string.Format(
"%1:%2", minutes.ToString(2), seconds.ToString(2));
1264 return string.Format(
"%1:%2:%3", hours, minutes.ToString(2), seconds.ToString(2));
1268 override void OnStringTableItemContextMenu()
1274class TranslationPlugin_LoadConfigUI
1277 defvalue: DEFAULT_MATCH_CONFIG,
1278 desc:
"Config to load into the Match Config field using the Load Config button - overrides any existing config",
1280 params:
"conf class=TranslationPluginMatchConfig")]
1281 ResourceName m_sConfigFilePath;
1283 protected static const ResourceName DEFAULT_MATCH_CONFIG =
"{9414DBD68A4429DB}Configs/Workbench/LocalizationEditor/TranslatePlugin/BaseStringTableItemConfig.conf";
1287 protected int ButtonLoad()
1299 void TranslationPlugin_LoadConfigUI()
1301 if (m_sConfigFilePath.IsEmpty())
1302 m_sConfigFilePath = DEFAULT_MATCH_CONFIG;
1306class TranslationPlugin_DataWrapper
1311 ref TranslationPluginQuery m_Query;
1314enum ETranslationPlugin_EditedRowsMode
1317 PROCESS_UNEDITED_ONLY,
1318 PROCESS_EDITED_ONLY,
1321enum ETranslationPlugin_ProcessMode
ArmaReforgerScripted GetGame()
ResourceName resourceName
UI Textures DeployMenu Briefing conflict_HintBanner_1_UI desc
class WorkbenchDialog_AbortRetryIgnore ButtonAttribute("OK", true)
Object holding reference to resource. In destructor release the resource.
Script accessible REST context.
Base class for scripted string table item.
proto void Print(void var, LogLevel level=LogLevel.NORMAL)
Prints content of variable to console/log.
LogLevel
Enum with severity of the logging message.
proto void PrintFormat(string fmt, void param1=NULL, void param2=NULL, void param3=NULL, void param4=NULL, void param5=NULL, void param6=NULL, void param7=NULL, void param8=NULL, void param9=NULL, LogLevel level=LogLevel.NORMAL)
SCR_FieldOfViewSettings Attribute
proto external string ToString()
Plain C++ pointer, no weak pointers, no memory management.
proto native bool IsEmpty()
ERestResult
States and result + error code produced by RestApi.