diff --git a/.clang-format b/.clang-format index 995dd08..df1cbc1 100644 --- a/.clang-format +++ b/.clang-format @@ -32,7 +32,7 @@ BreakBeforeBraces: Custom BinPackArguments: false BinPackParameters: false BreakConstructorInitializers: AfterColon -ColumnLimit: 200 +ColumnLimit: 1000 IncludeBlocks: Regroup IncludeCategories: - Regex: '.*\.generated\.h' diff --git a/Source/Tolgee/Private/Tolgee.cpp b/Source/Tolgee/Private/Tolgee.cpp index 9cb8991..565d892 100644 --- a/Source/Tolgee/Private/Tolgee.cpp +++ b/Source/Tolgee/Private/Tolgee.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #include "Tolgee.h" diff --git a/Source/Tolgee/Private/Tolgee.h b/Source/Tolgee/Private/Tolgee.h deleted file mode 100644 index eeb43aa..0000000 --- a/Source/Tolgee/Private/Tolgee.h +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#pragma once - -#include - -/** - * @brief Tolgee module is responsible for fetching translation data from the backend and updating it at runtime. - */ -class FTolgeeModule : public IModuleInterface -{ -}; diff --git a/Source/Tolgee/Private/TolgeeCdnFetcherSubsystem.cpp b/Source/Tolgee/Private/TolgeeCdnFetcherSubsystem.cpp new file mode 100644 index 0000000..36090a8 --- /dev/null +++ b/Source/Tolgee/Private/TolgeeCdnFetcherSubsystem.cpp @@ -0,0 +1,118 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#include "TolgeeCdnFetcherSubsystem.h" + +#include +#include +#include + +#include "TolgeeRuntimeSettings.h" +#include "TolgeeLog.h" + +void UTolgeeCdnFetcherSubsystem::OnGameInstanceStart(UGameInstance* GameInstance) +{ + const UTolgeeRuntimeSettings* Settings = GetDefault(); + if (Settings->CdnAddresses.IsEmpty()) + { + UE_LOG(LogTolgee, Display, TEXT("No CDN addresses configured. Packaged builds will only use static data.")); + return; + } + if (GIsEditor && !Settings->bUseCdnInEditor) + { + UE_LOG(LogTolgee, Display, TEXT("CDN was disabled for editor but it will be used in the final game.")); + return; + } + + FetchAllCdns(); +} + +void UTolgeeCdnFetcherSubsystem::OnGameInstanceEnd(bool bIsSimulating) +{ + ResetData(); + + LastModifiedDates.Empty(); +} + +TMap> UTolgeeCdnFetcherSubsystem::GetDataToInject() const +{ + return CachedTranslations; +} + +void UTolgeeCdnFetcherSubsystem::FetchAllCdns() +{ + const UTolgeeRuntimeSettings* Settings = GetDefault(); + + for (const FString& CdnAddress : Settings->CdnAddresses) + { + TArray Cultures = UKismetInternationalizationLibrary::GetLocalizedCultures(); + for (const FString& Culture : Cultures) + { + const FString DownloadUrl = FString::Printf(TEXT("%s/%s.po"), *CdnAddress, *Culture); + + UE_LOG(LogTolgee, Display, TEXT("Fetching localization data for culture: %s from CDN: %s"), *Culture, *DownloadUrl); + FetchFromCdn(Culture, DownloadUrl); + } + }; + +} + +void UTolgeeCdnFetcherSubsystem::FetchFromCdn(const FString& Culture, const FString& DownloadUrl) +{ + const FString* LastModifiedDate = LastModifiedDates.Find(DownloadUrl); + + const FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); + HttpRequest->SetVerb("GET"); + HttpRequest->SetURL(DownloadUrl); + HttpRequest->SetHeader(TEXT("accept"), TEXT("application/json")); + + if (LastModifiedDate) + { + HttpRequest->SetHeader(TEXT("If-Modified-Since"), *LastModifiedDate); + } + + HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnFetchedFromCdn, Culture); + HttpRequest->ProcessRequest(); + + NumRequestsSent++; +} + +void UTolgeeCdnFetcherSubsystem::OnFetchedFromCdn(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString InCutlure) +{ + if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) + { + UE_LOG(LogTolgee, Display, TEXT("Fetch successfully for %s to %s."), *InCutlure, *Request->GetURL()); + + TArray Translations = ExtractTranslationsFromPO(Response->GetContentAsString()); + CachedTranslations.Emplace(InCutlure, Translations); + + const FString LastModified = Response->GetHeader(TEXT("Last-Modified")); + if (!LastModified.IsEmpty()) + { + LastModifiedDates.Emplace(Request->GetURL(), LastModified); + } + } + else if (Response.IsValid() && Response->GetResponseCode() == EHttpResponseCodes::NotModified) + { + UE_LOG(LogTolgee, Display, TEXT("No new data for %s to %s."), *InCutlure, *Request->GetURL()); + } + else + { + UE_LOG(LogTolgee, Error, TEXT("Request for %s to %s failed."), *InCutlure, *Request->GetURL()); + } + + NumRequestsCompleted++; + + if (NumRequestsCompleted == NumRequestsSent) + { + UE_LOG(LogTolgee, Display, TEXT("All requests completed. Refreshing translation data.")); + RefreshTranslationDataAsync(); + } +} + +void UTolgeeCdnFetcherSubsystem::ResetData() +{ + NumRequestsSent = 0; + NumRequestsCompleted = 0; + + CachedTranslations.Empty(); +} \ No newline at end of file diff --git a/Source/Tolgee/Private/TolgeeLocalizationInjectorSubsystem.cpp b/Source/Tolgee/Private/TolgeeLocalizationInjectorSubsystem.cpp new file mode 100644 index 0000000..098f272 --- /dev/null +++ b/Source/Tolgee/Private/TolgeeLocalizationInjectorSubsystem.cpp @@ -0,0 +1,124 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#include "TolgeeLocalizationInjectorSubsystem.h" + +#include +#include +#include + +#if WITH_LOCALIZATION_MODULE +#include +#include +#endif + +#include "TolgeeLog.h" +#include "TolgeeTextSource.h" + +void UTolgeeLocalizationInjectorSubsystem::OnGameInstanceStart(UGameInstance* GameInstance) +{ +} + +void UTolgeeLocalizationInjectorSubsystem::OnGameInstanceEnd(bool bIsSimulating) +{ +} + +void UTolgeeLocalizationInjectorSubsystem::GetLocalizedResources(const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource) const +{ + TMap> DataToInject = GetDataToInject(); + for (const TPair>& CachedTranslation : DataToInject) + { + if (!InPrioritizedCultures.Contains(CachedTranslation.Key)) + { + continue; + } + + for (const FTolgeeTranslationData& TranslationData : CachedTranslation.Value) + { + const FTextKey InNamespace = TranslationData.ParsedNamespace; + const FTextKey InKey = TranslationData.ParsedKey; + const FString InLocalizedString = TranslationData.Translation; + + if (FTextLocalizationResource::FEntry* ExistingEntry = InOutLocalizedResource.Entries.Find(FTextId(InNamespace, InKey))) + { + //NOTE: -1 is a higher than usual priority, meaning this entry will override any existing one. See FTextLocalizationResource::ShouldReplaceEntry + InOutLocalizedResource.AddEntry(InNamespace, InKey, ExistingEntry->SourceStringHash, InLocalizedString, -1); + } + else + { + UE_LOG(LogTolgee, Warning, TEXT("Failed to inject translation for %s:%s. Default entry not found."), *InNamespace.ToString(), *InKey.ToString()); + } + } + } +} + +TMap> UTolgeeLocalizationInjectorSubsystem::GetDataToInject() const +{ + return {}; +} + +void UTolgeeLocalizationInjectorSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + TextSource = MakeShared(); + TextSource->GetLocalizedResources.BindUObject(this, &ThisClass::GetLocalizedResources); + FTextLocalizationManager::Get().RegisterTextSource(TextSource.ToSharedRef()); + + FWorldDelegates::OnStartGameInstance.AddUObject(this, &ThisClass::OnGameInstanceStart); + +#if WITH_EDITOR + FEditorDelegates::PrePIEEnded.AddUObject(this, &ThisClass::OnGameInstanceEnd); +#endif +} + +void UTolgeeLocalizationInjectorSubsystem::RefreshTranslationDataAsync() +{ + UE_LOG(LogTolgee, Verbose, TEXT("RefreshTranslationDataAsync requested.")); + + AsyncTask( + ENamedThreads::AnyHiPriThreadHiPriTask, + [=]() + { + TRACE_CPUPROFILER_EVENT_SCOPE(UTolgeeLocalizationInjectorSubsystem::RefreshResources) + + UE_LOG(LogTolgee, Verbose, TEXT("RefreshTranslationDataAsync executing.")); + + FTextLocalizationManager::Get().RefreshResources(); + } + ); +} + +TArray UTolgeeLocalizationInjectorSubsystem::ExtractTranslationsFromPO(const FString& PoContent) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UTolgeeLocalizationInjectorSubsystem::ExtractTranslationsFromPO) + + TArray Result; + +#if WITH_LOCALIZATION_MODULE + FPortableObjectFormatDOM PortableObject; + PortableObject.FromString(PoContent); + + for (auto EntryPairIter = PortableObject.GetEntriesIterator(); EntryPairIter; ++EntryPairIter) + { + auto POEntry = EntryPairIter->Value; + if (POEntry->MsgId.IsEmpty() || POEntry->MsgStr.Num() == 0 || POEntry->MsgStr[0].IsEmpty()) + { + // We ignore the header entry or entries with no translation. + continue; + } + + FTolgeeTranslationData TranslationData; + + constexpr ELocalizedTextCollapseMode InTextCollapseMode = ELocalizedTextCollapseMode::IdenticalTextIdAndSource; + constexpr EPortableObjectFormat InPOFormat = EPortableObjectFormat::Crowdin; + + PortableObjectPipeline::ParseBasicPOFileEntry(*POEntry, TranslationData.ParsedNamespace, TranslationData.ParsedKey, TranslationData.SourceText, TranslationData.Translation, InTextCollapseMode, InPOFormat); + + Result.Add(TranslationData); + } +#else + UE_LOG(LogTolgee, Error, TEXT("Localization module is not available. Cannot extract translations from PO content.")); +#endif + + return Result; +} \ No newline at end of file diff --git a/Source/Tolgee/Private/TolgeeLocalizationSubsystem.cpp b/Source/Tolgee/Private/TolgeeLocalizationSubsystem.cpp deleted file mode 100644 index 2f4efed..0000000 --- a/Source/Tolgee/Private/TolgeeLocalizationSubsystem.cpp +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#include "TolgeeLocalizationSubsystem.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "TolgeeLog.h" -#include "TolgeeRuntimeRequestData.h" -#include "TolgeeSettings.h" -#include "TolgeeTextSource.h" -#include "TolgeeUtils.h" - -void UTolgeeLocalizationSubsystem::ManualFetch() -{ - UE_LOG(LogTolgee, Log, TEXT("UTolgeeLocalizationSubsystem::ManualFetch")); - - FetchTranslation(); -} - -const FLocalizedDictionary& UTolgeeLocalizationSubsystem::GetLocalizedDictionary() const -{ - return LocalizedDictionary; -} - -void UTolgeeLocalizationSubsystem::GetLocalizedResources( - const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource -) const -{ - for (const FLocalizedKey& KeyData : LocalizedDictionary.Keys) - { - if (!InPrioritizedCultures.Contains(KeyData.Locale)) - { - continue; - } - - const FTextKey InNamespace = KeyData.Namespace; - const FTextKey InKey = KeyData.Name; - const uint32 InKeyHash = KeyData.Hash; - const FString InLocalizedString = KeyData.Translation; - - if (InKeyHash) - { - InOutLocalizedResource.AddEntry(InNamespace, InKey, InKeyHash, InLocalizedString, 0); - } - else - { - UE_LOG(LogTolgee, Error, TEXT("KeyData {Namespace: %s Key: %s} has invalid hash."), InNamespace.GetChars(), InKey.GetChars()); - } - } -} - -void UTolgeeLocalizationSubsystem::Initialize(FSubsystemCollectionBase& Collection) -{ - UE_LOG(LogTolgee, Log, TEXT("UTolgeeLocalizationSubsystem::Initialize")); - - Super::Initialize(Collection); - - // OnGameInstanceStart was introduced in 4.27, so we are trying to mimic the behavior for older version -#if UE_VERSION_OLDER_THAN(4, 27, 0) - UGameViewportClient::OnViewportCreated().AddWeakLambda(this, [this]() - { - const UGameViewportClient* ViewportClient = GEngine ? GEngine->GameViewport : nullptr; - UGameInstance* GameInstance = ViewportClient ? ViewportClient->GetGameInstance() : nullptr; - if(GameInstance) - { - UE_LOG(LogTolgee, Log, TEXT("Workaround for OnGameInstanceStart")); - OnGameInstanceStart(GameInstance); - } - else - { - UE_LOG(LogTolgee, Error, TEXT("Workaround for OnGameInstanceStart failed")); - } - }); -#else - FWorldDelegates::OnStartGameInstance.AddUObject(this, &ThisClass::OnGameInstanceStart); -#endif - - TextSource = MakeShared(); - TextSource->GetLocalizedResources.BindUObject(this, &ThisClass::GetLocalizedResources); - FTextLocalizationManager::Get().RegisterTextSource(TextSource.ToSharedRef()); -} - -void UTolgeeLocalizationSubsystem::OnGameInstanceStart(UGameInstance* GameInstance) -{ - const UTolgeeSettings* Settings = GetDefault(); - if (Settings->bLiveTranslationUpdates) - { - FetchTranslation(); - GameInstance->GetTimerManager().SetTimer(AutoFetchTimerHandle, this, &ThisClass::FetchTranslation, Settings->UpdateInterval, true); - } - else - { - LoadLocalData(); - } -} - -void UTolgeeLocalizationSubsystem::FetchTranslation() -{ - UE_LOG(LogTolgee, Verbose, TEXT("UTolgeeLocalizationSubsystem::FetchTranslation")); - - const UTolgeeSettings* Settings = GetDefault(); - if (!Settings->IsReadyToSendRequests()) - { - UE_LOG(LogTolgee, Warning, TEXT("Settings are not set up properly. Fetch request will be skipped.")); - return; - } - - if (bFetchInProgress) - { - UE_LOG(LogTolgee, Log, TEXT("Fetch skipped, an update is already in progress.")); - return; - } - - FOnTranslationFetched OnDoneCallback = FOnTranslationFetched::CreateUObject(this, &UTolgeeLocalizationSubsystem::OnAllTranslationsFetched); - bFetchInProgress = true; - - FetchNextTranslation(MoveTemp(OnDoneCallback), {}, {}); -} - -void UTolgeeLocalizationSubsystem::FetchNextTranslation(FOnTranslationFetched Callback, TArray CurrentTranslations, const FString& NextCursor) -{ - const UTolgeeSettings* Settings = GetDefault(); - - TArray QueryParameters; - if (!NextCursor.IsEmpty()) - { - QueryParameters.Emplace(FString::Printf(TEXT("cursor=%s"), *NextCursor)); - } - - for (const FString& Language : Settings->Languages) - { - QueryParameters.Emplace(FString::Printf(TEXT("languages=%s"), *Language)); - } - - const FString EndpointUrl = TolgeeUtils::GetUrlEndpoint(TEXT("v2/projects/translations")); - const FString RequestUrl = TolgeeUtils::AppendQueryParameters(EndpointUrl, QueryParameters); - const FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); - HttpRequest->SetVerb("GET"); - HttpRequest->SetHeader(TEXT("X-API-Key"), Settings->ApiKey); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Type"), TolgeeUtils::GetSdkType()); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Version"), TolgeeUtils::GetSdkVersion()); - HttpRequest->SetURL(RequestUrl); - HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnNextTranslationFetched, Callback, CurrentTranslations); - HttpRequest->ProcessRequest(); -} - -void UTolgeeLocalizationSubsystem::OnNextTranslationFetched( - FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FOnTranslationFetched Callback, TArray CurrentTranslations -) -{ - UE_LOG(LogTolgee, Verbose, TEXT("UTolgeeLocalizationSubsystem::OnNextTranslationFetched")); - - if (!bWasSuccessful) - { - bFetchInProgress = false; - UE_LOG(LogTolgee, Error, TEXT("Request to fetch translations was unsuccessful.")); - return; - } - - if (!EHttpResponseCodes::IsOk(Response->GetResponseCode())) - { - bFetchInProgress = false; - UE_LOG(LogTolgee, Error, TEXT("Request to fetch translation received unexpected code: %s"), *LexToString(Response->GetResponseCode())); - return; - } - - TSharedPtr JsonObject = MakeShared(); - const TSharedRef> JsonReader = TJsonReaderFactory::Create(Response->GetContentAsString()); - if (!FJsonSerializer::Deserialize(JsonReader, JsonObject)) - { - bFetchInProgress = false; - UE_LOG(LogTolgee, Error, TEXT("Could not deserialize response: %s"), *LexToString(Response->GetContentAsString())); - return; - } - - const FString NextCursor = [JsonObject]() - { - FString OutString; - JsonObject->TryGetStringField(TEXT("nextCursor"), OutString); - return OutString; - }(); - const TSharedPtr* EmbeddedData = [JsonObject]() - { - const TSharedPtr* OutObject = nullptr; - JsonObject->TryGetObjectField(TEXT("_embedded"), OutObject); - return OutObject; - }(); - const TArray> Keys = EmbeddedData ? (*EmbeddedData)->GetArrayField(TEXT("keys")) : TArray>(); - - for (const TSharedPtr& Key : Keys) - { - const TOptional KeyData = FTolgeeKeyData::FromJson(Key); - if (KeyData.IsSet()) - { - CurrentTranslations.Add(KeyData.GetValue()); - } - } - - const bool bAllKeysFetched = NextCursor.IsEmpty(); - if (bAllKeysFetched) - { - Callback.Execute(CurrentTranslations); - } - else - { - FetchNextTranslation(Callback, CurrentTranslations, NextCursor); - } -} - -void UTolgeeLocalizationSubsystem::OnAllTranslationsFetched(TArray Translations) -{ - LocalizedDictionary.Keys.Empty(); - - for (const FTolgeeKeyData& Translation : Translations) - { - for (const TTuple& Language : Translation.Translations) - { - FLocalizedKey& Key = LocalizedDictionary.Keys.Emplace_GetRef(); - Key.Name = Translation.KeyName; - Key.Namespace = Translation.KeyNamespace; - Key.Hash = Translation.GetKeyHash(); - Key.Locale = Language.Key; - Key.Translation = Language.Value.Text; - Key.RemoteId = Translation.KeyId; - } - } - - bFetchInProgress = false; - - RefreshTranslationData(); -} - -void UTolgeeLocalizationSubsystem::RefreshTranslationData() -{ - UE_LOG(LogTolgee, Verbose, TEXT("UTolgeeLocalizationSubsystem::RefreshTranslationData")); - - AsyncTask( - ENamedThreads::AnyHiPriThreadHiPriTask, - [=]() - { - TRACE_CPUPROFILER_EVENT_SCOPE(Tolgee::LocalizationManager::RefreshResources) - - UE_LOG(LogTolgee, Verbose, TEXT("Tolgee::LocalizationManager::RefreshResources")); - - FTextLocalizationManager::Get().RefreshResources(); - } - ); -} - -void UTolgeeLocalizationSubsystem::LoadLocalData() -{ - const FString Filename = TolgeeUtils::GetLocalizationSourceFile(); - FString JsonString; - if (!FFileHelper::LoadFileToString(JsonString, *Filename)) - { - UE_LOG(LogTolgee, Error, TEXT("Couldn't load local data from file: %s"), *Filename); - return; - } - - TSharedPtr JsonObject; - const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(JsonString); - if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid()) - { - UE_LOG(LogTolgee, Error, TEXT("Couldn't deserialize local data from file: %s"), *Filename); - return; - } - - if (!FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &LocalizedDictionary)) - { - UE_LOG(LogTolgee, Error, TEXT("Couldn't convert local data to struct from file: %s"), *Filename); - return; - } - - UE_LOG(LogTolgee, Log, TEXT("Local data loaded successfully from file: %s"), *Filename); - - RefreshTranslationData(); -} \ No newline at end of file diff --git a/Source/Tolgee/Private/TolgeeLog.cpp b/Source/Tolgee/Private/TolgeeLog.cpp index e47c05f..185253e 100644 --- a/Source/Tolgee/Private/TolgeeLog.cpp +++ b/Source/Tolgee/Private/TolgeeLog.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #include "TolgeeLog.h" diff --git a/Source/Tolgee/Private/TolgeeRuntimeRequestData.cpp b/Source/Tolgee/Private/TolgeeRuntimeRequestData.cpp deleted file mode 100644 index fbd2ac5..0000000 --- a/Source/Tolgee/Private/TolgeeRuntimeRequestData.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#include "TolgeeRuntimeRequestData.h" - -#include - -#include "TolgeeUtils.h" - -TOptional FTolgeeKeyData::FromJson(TSharedPtr InValue) -{ - if (!InValue.IsValid()) - { - return {}; - } - - FTolgeeKeyData OutData; - const TSharedRef TranslatedKeyObject = InValue->AsObject().ToSharedRef(); - if (!FJsonObjectConverter::JsonObjectToUStruct(TranslatedKeyObject, &OutData)) - { - return {}; - } - - const TSharedPtr TranslationJsonData = TranslatedKeyObject->GetObjectField(TEXT("translations")); - for (const auto& TranslationData : TranslationJsonData->Values) - { - TSharedPtr Translation = TranslationData.Value; - if (Translation.IsValid()) - { - FTolgeeTranslation CurrentLanguage; - FJsonObjectConverter::JsonObjectToUStruct(Translation->AsObject().ToSharedRef(), &CurrentLanguage); - OutData.Translations.Emplace(TranslationData.Key, CurrentLanguage); - } - } - - return OutData; -} - -TArray FTolgeeKeyData::GetAvailableLanguages() const -{ - TArray Result; - Translations.GenerateKeyArray(Result); - return Result; -} - -uint32 FTolgeeKeyData::GetKeyHash() const -{ - const FString KeyHashValue = GetTagValue(TolgeeUtils::KeyHashPrefix); - - const uint32 KeyHash = static_cast(FCString::Atoi64(*KeyHashValue)); - return KeyHash; -} - -FString FTolgeeKeyData::GetTagValue(const FString& Prefix) const -{ - const FTolgeeKeyTag* FoundTag = KeyTags.FindByPredicate( - [Prefix](const FTolgeeKeyTag& TagText) - { - return TagText.Name.Contains(Prefix); - } - ); - - if (!FoundTag) - { - return {}; - } - - FString TagValue = FoundTag->Name; - TagValue.RemoveFromStart(Prefix); - return TagValue; -} \ No newline at end of file diff --git a/Source/Tolgee/Private/TolgeeRuntimeSettings.cpp b/Source/Tolgee/Private/TolgeeRuntimeSettings.cpp new file mode 100644 index 0000000..bdeaaa7 --- /dev/null +++ b/Source/Tolgee/Private/TolgeeRuntimeSettings.cpp @@ -0,0 +1,8 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#include "TolgeeRuntimeSettings.h" + +FName UTolgeeRuntimeSettings::GetCategoryName() const +{ + return TEXT("Plugins"); +} \ No newline at end of file diff --git a/Source/Tolgee/Private/TolgeeSettings.cpp b/Source/Tolgee/Private/TolgeeSettings.cpp deleted file mode 100644 index 4dc61ba..0000000 --- a/Source/Tolgee/Private/TolgeeSettings.cpp +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#include "TolgeeSettings.h" - -#include -#include - -#include "TolgeeLog.h" -#include "TolgeeUtils.h" - -#if WITH_EDITOR -#include -#endif - -#if WITH_EDITOR -void UTolgeeSettings::OpenSettings() -{ - const UTolgeeSettings* Settings = GetDefault(); - - ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); - SettingsModule.ShowViewer(Settings->GetContainerName(), Settings->GetCategoryName(), Settings->GetSectionName()); -} -#endif - -bool UTolgeeSettings::IsReadyToSendRequests() const -{ - return !ApiKey.IsEmpty() && !ApiUrl.IsEmpty() && !ProjectId.IsEmpty(); -} - -FName UTolgeeSettings::GetContainerName() const -{ - return TEXT("Project"); -} - -FName UTolgeeSettings::GetCategoryName() const -{ - return TEXT("Localization"); -} - -FName UTolgeeSettings::GetSectionName() const -{ - return TEXT("Tolgee"); -} - -#if WITH_EDITOR -FText UTolgeeSettings::GetSectionText() const -{ - const FName DisplaySectionName = GetSectionName(); - return FText::FromName(DisplaySectionName); -} -#endif - -#if WITH_EDITOR -void UTolgeeSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UTolgeeSettings, ApiKey)) - { - FetchProjectId(); - } -} -#endif - -void UTolgeeSettings::FetchProjectId() -{ - UE_LOG(LogTolgee, Verbose, TEXT("UTolgeeSettings::FetchProjectId")); - - const FString RequestUrl = TolgeeUtils::GetUrlEndpoint(TEXT("v2/api-keys/current")); - const FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); - HttpRequest->SetVerb("GET"); - HttpRequest->SetHeader(TEXT("X-API-Key"), ApiKey); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Type"), TolgeeUtils::GetSdkType()); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Version"), TolgeeUtils::GetSdkVersion()); - HttpRequest->SetURL(RequestUrl); - HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnProjectIdFetched); - HttpRequest->ProcessRequest(); -} - -void UTolgeeSettings::OnProjectIdFetched(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) -{ - UE_LOG(LogTolgee, Verbose, TEXT("UTolgeeSettings::OnProjectIdFetched")); - - if (!bWasSuccessful) - { - UE_LOG(LogTolgee, Error, TEXT("Request to fetch ProjectId was unsuccessful.")); - ProjectId = TEXT("INVALID"); - return; - } - - if (!EHttpResponseCodes::IsOk(Response->GetResponseCode())) - { - UE_LOG(LogTolgee, Error, TEXT("Request to fetch ProjectId received unexpected code: %s"), *LexToString(Response->GetResponseCode())); - ProjectId = TEXT("INVALID"); - return; - } - - TSharedPtr JsonObject = MakeShared(); - const TSharedRef> JsonReader = TJsonReaderFactory::Create(Response->GetContentAsString()); - if (!FJsonSerializer::Deserialize(JsonReader, JsonObject)) - { - UE_LOG(LogTolgee, Error, TEXT("Could not deserialize response: %s"), *LexToString(Response->GetContentAsString())); - ProjectId = TEXT("INVALID"); - return; - } - - ProjectId = [JsonObject]() - { - int64 ProjectIdValue; - JsonObject->TryGetNumberField(TEXT("projectId"), ProjectIdValue); - return LexToString(ProjectIdValue); - }(); - - SaveToDefaultConfig(); - - FetchDefaultLanguages(); -} - -void UTolgeeSettings::FetchDefaultLanguages() -{ - UE_LOG(LogTolgee, Verbose, TEXT("UTolgeeSettings::FetchDefaultLanguages")); - - const FString RequestUrl = TolgeeUtils::GetUrlEndpoint(TEXT("v2/projects/stats")); - const FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); - HttpRequest->SetVerb("GET"); - HttpRequest->SetHeader(TEXT("X-API-Key"), ApiKey); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Type"), TolgeeUtils::GetSdkType()); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Version"), TolgeeUtils::GetSdkVersion()); - HttpRequest->SetURL(RequestUrl); - HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnDefaultLanguagesFetched); - HttpRequest->ProcessRequest(); -} - -void UTolgeeSettings::OnDefaultLanguagesFetched(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) -{ - UE_LOG(LogTolgee, Verbose, TEXT("UTolgeeSettings::OnDefaultLanguagesFetched")); - - if (!bWasSuccessful) - { - UE_LOG(LogTolgee, Error, TEXT("Request to fetch default languages was unsuccessful.")); - return; - } - - if (!EHttpResponseCodes::IsOk(Response->GetResponseCode())) - { - UE_LOG(LogTolgee, Error, TEXT("Request to fetch default languages received unexpected code: %s"), *LexToString(Response->GetResponseCode())); - return; - } - - TSharedPtr JsonObject = MakeShared(); - const TSharedRef> JsonReader = TJsonReaderFactory::Create(Response->GetContentAsString()); - if (!FJsonSerializer::Deserialize(JsonReader, JsonObject)) - { - UE_LOG(LogTolgee, Error, TEXT("Could not deserialize response: %s"), *LexToString(Response->GetContentAsString())); - return; - } - - Languages.Empty(); - - TArray> LanguageStats = JsonObject->GetArrayField(TEXT("languageStats")); - for (const TSharedPtr& Language : LanguageStats) - { - FString LanguageLocale; - Language->AsObject()->TryGetStringField(TEXT("languageTag"), LanguageLocale); - Languages.Add(LanguageLocale); - } - - SaveToDefaultConfig(); -} - -void UTolgeeSettings::SaveToDefaultConfig() -{ - SaveConfig(CPF_Config, *GetDefaultConfigFilename()); -} diff --git a/Source/Tolgee/Private/TolgeeTextSource.cpp b/Source/Tolgee/Private/TolgeeTextSource.cpp index d200637..c689096 100644 --- a/Source/Tolgee/Private/TolgeeTextSource.cpp +++ b/Source/Tolgee/Private/TolgeeTextSource.cpp @@ -1,8 +1,16 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #include "TolgeeTextSource.h" -#include "Internationalization/TextLocalizationResource.h" +#include + +int32 FTolgeeTextSource::GetPriority() const +{ + //NOTE: you might expect to see ::Highest, but we actually want to be the last one to load resources so we can re-use the existing SourceStringHash + // Then, during the load we will inject the new entries the highest priority. + return ELocalizedTextSourcePriority::Lowest; +} + bool FTolgeeTextSource::GetNativeCultureName(const ELocalizedTextSourceCategory InCategory, FString& OutNativeCultureName) { // TODO: Investigate if we should implement this @@ -14,9 +22,7 @@ void FTolgeeTextSource::GetLocalizedCultureNames(const ELocalizationLoadFlags In // TODO: Investigate if we should implement this } -void FTolgeeTextSource::LoadLocalizedResources( - const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource -) +void FTolgeeTextSource::LoadLocalizedResources(const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource) { GetLocalizedResources.Execute(InLoadFlags, InPrioritizedCultures, InOutNativeResource, InOutLocalizedResource); } \ No newline at end of file diff --git a/Source/Tolgee/Private/TolgeeTextSource.h b/Source/Tolgee/Private/TolgeeTextSource.h deleted file mode 100644 index 2417471..0000000 --- a/Source/Tolgee/Private/TolgeeTextSource.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#pragma once - -#include - -using FGetLocalizedResources = TDelegate< - void(const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource)>; - -/** - * Translation source data for providing data fetched from Tolgee backend - */ -class FTolgeeTextSource : public ILocalizedTextSource -{ -public: - /** - * @brief Callback executed when the text source needs to load the localized resources - */ - FGetLocalizedResources GetLocalizedResources; - -private: - // Begin ILocalizedTextSource interface - virtual int32 GetPriority() const override { return ELocalizedTextSourcePriority::Highest; } - virtual bool GetNativeCultureName(const ELocalizedTextSourceCategory InCategory, FString& OutNativeCultureName) override; - virtual void GetLocalizedCultureNames(const ELocalizationLoadFlags InLoadFlags, TSet& OutLocalizedCultureNames) override; - virtual void LoadLocalizedResources( - const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource - ) override; - // End ILocalizedTextSource interface -}; diff --git a/Source/Tolgee/Private/TolgeeUtils.cpp b/Source/Tolgee/Private/TolgeeUtils.cpp index 15de4c6..49da7ae 100644 --- a/Source/Tolgee/Private/TolgeeUtils.cpp +++ b/Source/Tolgee/Private/TolgeeUtils.cpp @@ -1,17 +1,8 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #include "TolgeeUtils.h" #include -#include -#include - -#include "TolgeeSettings.h" - -uint32 TolgeeUtils::GetTranslationHash(const FString& Translation) -{ - return FTextLocalizationResource::HashString(Translation); -} FString TolgeeUtils::AppendQueryParameters(const FString& BaseUrl, const TArray& Parameters) { @@ -32,19 +23,6 @@ FString TolgeeUtils::AppendQueryParameters(const FString& BaseUrl, const TArray< return FinalUrl; } -FString TolgeeUtils::GetUrlEndpoint(const FString& EndPoint) -{ - const FString& BaseUrl = GetDefault()->ApiUrl; - return FString::Printf(TEXT("%s/%s"), *BaseUrl, *EndPoint); -} - -FString TolgeeUtils::GetProjectUrlEndpoint(const FString& EndPoint /* = TEXT("") */) -{ - const FString ProjectId = GetDefault()->ProjectId; - const FString ProjectEndPoint = FString::Printf(TEXT("projects/%s/%s"), *ProjectId, *EndPoint); - return GetUrlEndpoint(ProjectEndPoint); -} - FString TolgeeUtils::GetSdkType() { return TEXT("Unreal"); @@ -56,14 +34,8 @@ FString TolgeeUtils::GetSdkVersion() return TolgeePlugin->GetDescriptor().VersionName; } -FString TolgeeUtils::GetLocalizationSourceFile() +void TolgeeUtils::AddSdkHeaders(FHttpRequestRef& HttpRequest) { - return FPaths::ProjectContentDir() + TEXT("Tolgee/Translations.json"); -} - -FDirectoryPath TolgeeUtils::GetLocalizationDirectory() -{ - FString Foldername = FPaths::GetPath(GetLocalizationSourceFile()); - FPaths::MakePathRelativeTo(Foldername, *FPaths::ProjectContentDir()); - return {Foldername}; -} + HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Type"), GetSdkType()); + HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Version"), GetSdkVersion()); +} \ No newline at end of file diff --git a/Source/Tolgee/Public/Tolgee.h b/Source/Tolgee/Public/Tolgee.h new file mode 100644 index 0000000..4219943 --- /dev/null +++ b/Source/Tolgee/Public/Tolgee.h @@ -0,0 +1,12 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include + +/** + * Base module for common Tolgee functionality and runtime features. + */ +class FTolgeeModule : public IModuleInterface +{ +}; diff --git a/Source/Tolgee/Public/TolgeeCdnFetcherSubsystem.h b/Source/Tolgee/Public/TolgeeCdnFetcherSubsystem.h new file mode 100644 index 0000000..78e777c --- /dev/null +++ b/Source/Tolgee/Public/TolgeeCdnFetcherSubsystem.h @@ -0,0 +1,57 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include "TolgeeLocalizationInjectorSubsystem.h" + +#include + +#include "TolgeeCdnFetcherSubsystem.generated.h" + +/** + * Subsystem responsible for fetching localization data from a CDN and injecting it into the game. + */ +UCLASS() +class UTolgeeCdnFetcherSubsystem : public UTolgeeLocalizationInjectorSubsystem +{ + GENERATED_BODY() + + // ~ Begin UTolgeeLocalizationInjectorSubsystem interface + virtual void OnGameInstanceStart(UGameInstance* GameInstance) override; + virtual void OnGameInstanceEnd(bool bIsSimulating) override; + virtual TMap> GetDataToInject() const override; + // ~ End UTolgeeLocalizationInjectorSubsystem interface + + /** + * Runs multiple requests to fetch all projects from the CDN. + */ + void FetchAllCdns(); + /** + * Fetches the localization data from the CDN. + */ + void FetchFromCdn(const FString& Culture, const FString& DownloadUrl); + /** + * Callback function for when the CDN request is completed. + */ + void OnFetchedFromCdn(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString InCutlure); + /** + * Clears the cached translations and resets the request counters. + */ + void ResetData(); + /** + * List of cached translations for each culture. + */ + TMap> CachedTranslations; + /** + * Counts the number of requests sent. + */ + int32 NumRequestsSent = 0; + /** + * Counts the number of requests completed. + */ + int32 NumRequestsCompleted = 0; + /** + * Map storing the last modified dates of the translations. + */ + TMap LastModifiedDates; +}; \ No newline at end of file diff --git a/Source/Tolgee/Public/TolgeeLocalizationInjectorSubsystem.h b/Source/Tolgee/Public/TolgeeLocalizationInjectorSubsystem.h new file mode 100644 index 0000000..2816be5 --- /dev/null +++ b/Source/Tolgee/Public/TolgeeLocalizationInjectorSubsystem.h @@ -0,0 +1,66 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include + +#include "TolgeeLocalizationInjectorSubsystem.generated.h" + +class UGameInstance; +class FTolgeeTextSource; + +/** + * Holds the parsed data from the PO file + */ +struct FTolgeeTranslationData +{ + FString ParsedNamespace; + FString ParsedKey; + FString SourceText; + FString Translation; +}; + +/** + * Base class for all subsystems responsible for dynamically injecting data at runtime (e.g.: CDN, Dashboard, etc.) + */ +UCLASS(Abstract) +class TOLGEE_API UTolgeeLocalizationInjectorSubsystem : public UEngineSubsystem +{ + GENERATED_BODY() + +protected: + /** + * Callback executed when the game instance is created and started. + */ + virtual void OnGameInstanceStart(UGameInstance* GameInstance); + /** + * Callback executed when the game instance is destroyed. + */ + virtual void OnGameInstanceEnd(bool bIsSimulating); + /** + * Callback executed when the TolgeeTextSource needs to load the localized resources. + */ + virtual void GetLocalizedResources(const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource) const; + /** + * Simplified getter to allow subclasses to provide their own data to inject for GetLocalizedResources. + */ + virtual TMap> GetDataToInject() const; + /** + * Triggers an async refresh of the LocalizationManager resources. + */ + void RefreshTranslationDataAsync(); + /** + * Converts PO content to a list of translation data. + */ + TArray ExtractTranslationsFromPO(const FString& PoContent); + + // Begin UEngineSubsystem interface + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + // End UEngineSubsystem interface + +private: + /** + * Custom Localization Text Source that allows handling of Localized Resources via delegate + */ + TSharedPtr TextSource; +}; diff --git a/Source/Tolgee/Public/TolgeeLocalizationSubsystem.h b/Source/Tolgee/Public/TolgeeLocalizationSubsystem.h deleted file mode 100644 index 20ae86a..0000000 --- a/Source/Tolgee/Public/TolgeeLocalizationSubsystem.h +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#pragma once - -#include -#include -#include -#include -#include - -#include "TolgeeRuntimeRequestData.h" - -#include "TolgeeLocalizationSubsystem.generated.h" - -class FTolgeeTextSource; - -using FOnTranslationFetched = TDelegate Translations)>; - -/** - * Subsystem responsible for fetching the latest translation data from Tolgee backend and applying it inside Unreal. - */ -UCLASS() -class TOLGEE_API UTolgeeLocalizationSubsystem : public UEngineSubsystem -{ - GENERATED_BODY() - -public: - /** - * @brief Immediately sends a fetch request to grab the latest data - */ - UFUNCTION(BlueprintCallable, Category = "Tolgee Localization") - void ManualFetch(); - /** - * @brief Returns true if a request to retrieve the translation from the backend server is currently ongoing - */ - bool IsFetchInprogress() const { return bFetchInProgress; } - /** - * @brief Returns the localized dictionary used the TolgeeTextSource - */ - const FLocalizedDictionary& GetLocalizedDictionary() const; - -private: - // Begin UEngineSubsystem interface - virtual void Initialize(FSubsystemCollectionBase& Collection) override; - // End UEngineSubsystem interface - /** - * @brief Callback executed when the TolgeeTextSource needs to update the translation data based on the last fetched keys. - * @param InLoadFlags - * @param InPrioritizedCultures - * @param InOutNativeResource - * @param InOutLocalizedResource - */ - void GetLocalizedResources( - const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource - ) const; - /** - * @brief Callback executed when the game instance is created and started. - */ - void OnGameInstanceStart(UGameInstance* GameInstance); - /** - * @brief Starts the fetching process to get the latest translated version. - */ - void FetchTranslation(); - /** - * @brief Sends a GET request based on the current cursor to fetch the remaining keys. - * @param Callback to be executed when all the keys are fetched - * @param CurrentTranslations Keys retrieved by previous requests - * @param NextCursor Indicator for the start of the next requests - */ - void FetchNextTranslation(FOnTranslationFetched Callback, TArray CurrentTranslations, const FString& NextCursor); - /** - * @brief Callback executed when a translation fetch is retrieved. - * @param Request Request that triggered this callback - * @param Response Response retrieved from the backend - * @param bWasSuccessful Request status - * @param Callback Callback to be executed when all the keys are fetched - * @param CurrentTranslations Keys retrieved by previous requests - */ - void OnNextTranslationFetched(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FOnTranslationFetched Callback, TArray CurrentTranslations); - /** - * @brief Callback executed when a fetch from the Tolgee backend is complete (all translations are fetched successfully) - * @param Translations Translation data received from the backend - */ - void OnAllTranslationsFetched(TArray Translations); - /** - * @brief Loads the translation data from a local file on disk. - */ - void LoadLocalData(); - /** - * @brief Triggers an async refresh of the LocalizationManager resources. - */ - void RefreshTranslationData(); - /** - * @brief Data used for localization by the TolgeeTextSource - * @note Could be coming from the Tolgee backend or locally saved - */ - FLocalizedDictionary LocalizedDictionary; - /** - * @brief Timer responsible for periodically fetching the latest keys. - */ - FTimerHandle AutoFetchTimerHandle; - /** - * @brief TextSource hooked into the LocalizationManager to feed in the latest keys in the translation data. - */ - TSharedPtr TextSource; - /** - * @brief Are we processing an update from the backend right now? Used to prevent 2 concurrent updates - */ - bool bFetchInProgress = false; -}; diff --git a/Source/Tolgee/Public/TolgeeLog.h b/Source/Tolgee/Public/TolgeeLog.h index 5653831..8b24557 100644 --- a/Source/Tolgee/Public/TolgeeLog.h +++ b/Source/Tolgee/Public/TolgeeLog.h @@ -1,4 +1,4 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #pragma once diff --git a/Source/Tolgee/Public/TolgeeRuntimeRequestData.h b/Source/Tolgee/Public/TolgeeRuntimeRequestData.h deleted file mode 100644 index 0d53d0d..0000000 --- a/Source/Tolgee/Public/TolgeeRuntimeRequestData.h +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#pragma once - -#include - -#include "TolgeeRuntimeRequestData.generated.h" - -class FJsonValue; - -/** - * @brief Representation of a Tolgee tag - * @see https://tolgee.io/platform/translation_keys/tags - */ -USTRUCT() -struct TOLGEE_API FTolgeeKeyTag -{ - GENERATED_BODY() - /** - * @brief Unique Id of the tag - */ - UPROPERTY() - int64 Id; - /** - * @brief Display name of the tag - */ - UPROPERTY() - FString Name; -}; - -/** - * @brief Representation of a Tolgee translation - */ -USTRUCT() -struct TOLGEE_API FTolgeeTranslation -{ - GENERATED_BODY() - /** - * @brief Unique Id of the translation - */ - UPROPERTY() - int64 Id; - /** - * @brief Translated text - */ - UPROPERTY() - FString Text; -}; - -/** - * @brief Representation of a Tolgee key - */ -USTRUCT() -struct TOLGEE_API FTolgeeKeyData -{ - GENERATED_BODY() - /** - * @brief Helper constructor to generate data from a JSON payload - */ - static TOptional FromJson(TSharedPtr InValue); - /** - * @brief Unique Id of the key - */ - UPROPERTY() - int64 KeyId; - /** - * @brief Name of the key - */ - UPROPERTY() - FString KeyName; - /** - * @brief Unique Id of the key's namespace - */ - UPROPERTY() - int64 KeyNamespaceId; - /** - * @brief Display name of the key's namespace - */ - UPROPERTY() - FString KeyNamespace; - /** - * @brief Tags assigned to the current key - */ - UPROPERTY() - TArray KeyTags; - /** - * @brief Map pairing available locales with their respective translation information - */ - TMap Translations; - /** - * @brief Gets all the available locales we have valid translations for - */ - TArray GetAvailableLanguages() const; - /** - * @brief Returns the original key's hash based on the current assigned tags. Or 0 if no valid tag was found. - */ - uint32 GetKeyHash() const; - -private: - /** - * @brief Helper method to get the value of a specific tag based on the tag's Prefix - * @param Prefix Key used inside the tag (e.g.: "Hash:" or "Text:" - * @return Rest of the tag's string after the prefix is removed. - */ - FString GetTagValue(const FString& Prefix) const; -}; - -/** - * Representation of a successfully localized key - */ -USTRUCT() -struct FLocalizedKey -{ - GENERATED_BODY() - - /** - * Name of the key - */ - UPROPERTY() - FString Name; - /** - * Namespace of the key - */ - UPROPERTY() - FString Namespace; - /** - * Unique hash of the key (calculated based on the source text) - */ - UPROPERTY() - uint32 Hash; - /** - * Language this key is translated for - */ - UPROPERTY() - FString Locale; - /** - * Translation for the specified locale - */ - UPROPERTY() - FString Translation; - /** - * Unique id used by the Tolgee backend to identify the key. - */ - TOptional RemoteId; -}; - -/** - * Representation of a translation dictionary (multiple keys for multiple languages) - */ -USTRUCT() -struct FLocalizedDictionary -{ - GENERATED_BODY() - - /** - * Localization keys for all languages - */ - UPROPERTY() - TArray Keys; -}; \ No newline at end of file diff --git a/Source/Tolgee/Public/TolgeeRuntimeSettings.h b/Source/Tolgee/Public/TolgeeRuntimeSettings.h new file mode 100644 index 0000000..e9e988c --- /dev/null +++ b/Source/Tolgee/Public/TolgeeRuntimeSettings.h @@ -0,0 +1,34 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include + +#include "TolgeeRuntimeSettings.generated.h" + +/** + * @brief Settings for the Tolgee runtime functionality. + * NOTE: Settings here will be packaged in the final game + */ +UCLASS(config = Tolgee, defaultconfig) +class TOLGEE_API UTolgeeRuntimeSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + /** + * Root addresses we should use to fetch the localization data from the CDN. + */ + UPROPERTY(Config, EditAnywhere, Category = "Tolgee|CDN") + TArray CdnAddresses; + + /** + * Easy toggle to disable the CDN functionality in the editor. + */ + UPROPERTY(Config, EditAnywhere, Category = "Tolgee|CDN") + bool bUseCdnInEditor = false; + + // ~ Begin UDeveloperSettings Interface + virtual FName GetCategoryName() const override; + // ~ End UDeveloperSettings Interface +}; \ No newline at end of file diff --git a/Source/Tolgee/Public/TolgeeSettings.h b/Source/Tolgee/Public/TolgeeSettings.h deleted file mode 100644 index bae72ad..0000000 --- a/Source/Tolgee/Public/TolgeeSettings.h +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#pragma once - -#include -#include - -#include "TolgeeSettings.generated.h" - -/** - * Holds configuration for integrating the Tolgee localization source - */ -UCLASS(config = Tolgee, defaultconfig) -class TOLGEE_API UTolgeeSettings : public UDeveloperSettings -{ - GENERATED_BODY() - -public: -#if WITH_EDITOR - /** - * @brief Shortcut to open the project settings windows focused to this config - */ - static void OpenSettings(); -#endif - /** - * @brief Checks if all the conditions to send requests to the Tolgee backend are met - * @note ApiKey, ApiUrl & ProjectId set properly - */ - bool IsReadyToSendRequests() const; - /* - * @brief Authentication key for fetching translation data - * @see https://tolgee.io/platform/account_settings/api_keys_and_pat_tokens - */ - UPROPERTY(Config, EditAnywhere, Category = "Tolgee Localization") - FString ApiKey = TEXT(""); - /** - * @brief Url of the Tolgee server. Useful for self-hosted installations - */ - UPROPERTY(Config, EditAnywhere, Category = "Tolgee Localization") - FString ApiUrl = TEXT("https://app.tolgee.io"); - /** - * @brief Id of the Tolgee project we should use for translation - */ - UPROPERTY(Config, VisibleAnywhere, Category = "Tolgee Localization") - FString ProjectId = TEXT(""); - /** - * @brief Locales we should fetch translations for - */ - UPROPERTY(Config, EditAnywhere, Category = "Tolgee Localization") - TArray Languages; - /** - * @brief With this enabled only strings from string tables will be uploaded & translated. Generated Keys (produced by Unreal for strings outside of string tables) will be ignored. - */ - UPROPERTY(Config, EditAnywhere, Category = "Tolgee Localization") - bool bIgnoreGeneratedKeys = true; - /** - * @brief Should we automatically fetch translation data at runtime? - */ - UPROPERTY(Config, EditAnywhere, Category = "Tolgee Localization") - bool bLiveTranslationUpdates = true; - /** - * @brief How often we should update the translation data from the server - * @note in seconds - */ - UPROPERTY(Config, EditAnywhere, Category = "Tolgee Localization", meta = (EditCondition = "bLiveTranslationUpdates", UIMin = "0")) - float UpdateInterval = 60.0f; - /** - * @brief Automatically fetch Translations.json on cook - */ - UPROPERTY(Config, EditAnywhere, Category = "Tolgee Localization") - bool bFetchTranslationsOnCook = true; -private: - // Begin UDeveloperSettings interface - virtual FName GetContainerName() const override; - virtual FName GetCategoryName() const override; - virtual FName GetSectionName() const override; -#if WITH_EDITOR - virtual FText GetSectionText() const override; - virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; -#endif - // End UDeveloperSettings interface - - /** - * @brief Sends a request to the Tolgee server to get information about the current project based on the ApiKey - */ - void FetchProjectId(); - /** - * @brief Callback executed when the information about the current project is retried - */ - void OnProjectIdFetched(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); - /** - * @brief Sends a request to the Tolgee server to get language information about the current project based on the ApiKey - */ - void FetchDefaultLanguages(); - /** - * @brief Callback executed when the language information about the current project is retrieved - */ - void OnDefaultLanguagesFetched(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); - /** - * @brief Saves the current values to the .ini file inside root Config folder - */ - void SaveToDefaultConfig(); -}; diff --git a/Source/Tolgee/Public/TolgeeTextSource.h b/Source/Tolgee/Public/TolgeeTextSource.h new file mode 100644 index 0000000..2194942 --- /dev/null +++ b/Source/Tolgee/Public/TolgeeTextSource.h @@ -0,0 +1,29 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include + +#include + +using FGetLocalizedResources = TDelegate InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource)>; + +/** + * Translation source data for injecting data fetched from Tolgee backend into the localization system. + */ +class TOLGEE_API FTolgeeTextSource : public ILocalizedTextSource +{ +public: + /** + * Callback executed when the text source needs to load the localized resources + */ + FGetLocalizedResources GetLocalizedResources; + +private: + // Begin ILocalizedTextSource interface + virtual int32 GetPriority() const override; + virtual bool GetNativeCultureName(const ELocalizedTextSourceCategory InCategory, FString& OutNativeCultureName) override; + virtual void GetLocalizedCultureNames(const ELocalizationLoadFlags InLoadFlags, TSet& OutLocalizedCultureNames) override; + virtual void LoadLocalizedResources(const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource) override; + // End ILocalizedTextSource interface +}; diff --git a/Source/Tolgee/Public/TolgeeUtils.h b/Source/Tolgee/Public/TolgeeUtils.h index 4a9c0a0..ea9aabb 100644 --- a/Source/Tolgee/Public/TolgeeUtils.h +++ b/Source/Tolgee/Public/TolgeeUtils.h @@ -1,28 +1,17 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #pragma once #include +#include + namespace TolgeeUtils { - /** - * @brief Utility to produce a hash for a string (as used by SourceStringHash) - */ - uint32 TOLGEE_API GetTranslationHash(const FString& Translation); /** * @brief Constructs an endpoint by appending query parameters to a base url */ FString TOLGEE_API AppendQueryParameters(const FString& BaseUrl, const TArray& Parameters); - /** - * @brief Creates the endpoint url for an API call based on an endpoint path and the currently assigned ApiUrl - */ - FString TOLGEE_API GetUrlEndpoint(const FString& EndPoint); - /** - * @brief Creates the endpoint url for a project API call based on an endpoint path and the currently assigned ApiUrl and ProjectId - * @note Can be used with an empty endpoint path to get the link to the web dashboard. - */ - FString TOLGEE_API GetProjectUrlEndpoint(const FString& EndPoint = TEXT("")); /** * @brief Sdk type of the Tolgee integration */ @@ -32,14 +21,7 @@ namespace TolgeeUtils */ FString TOLGEE_API GetSdkVersion(); /** - * @brief Returns the path to the file on disk where the localization is stored (Tolgee.json) + * Adds the Tolgee SDK type and version to the headers of the request */ - FString TOLGEE_API GetLocalizationSourceFile(); - /** - * @brief Returns the Directory path where the localization file is stored - * @note used by the Tolgee Editor Susbstem to inject this into the folders staged inside the .pak file - */ - FDirectoryPath TOLGEE_API GetLocalizationDirectory(); - - const FString KeyHashPrefix = TEXT("OriginalHash:"); + void TOLGEE_API AddSdkHeaders(FHttpRequestRef& HttpRequest); } // namespace TolgeeUtils diff --git a/Source/Tolgee/Tolgee.Build.cs b/Source/Tolgee/Tolgee.Build.cs index 7c61e48..19272a3 100644 --- a/Source/Tolgee/Tolgee.Build.cs +++ b/Source/Tolgee/Tolgee.Build.cs @@ -1,29 +1,39 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. using UnrealBuildTool; public class Tolgee : ModuleRules { - public Tolgee(ReadOnlyTargetRules Target) : base(Target) - { - PublicDependencyModuleNames.AddRange( - new string[] - { - "Core", - "DeveloperSettings", - "HTTP", - } - ); + public Tolgee(ReadOnlyTargetRules Target) : base(Target) + { + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "DeveloperSettings", + "Engine", + "HTTP", + "Json", + "JsonUtilities", + "Projects", + } + ); - PrivateDependencyModuleNames.AddRange( - new string[] - { - "CoreUObject", - "Engine", - "Json", - "JsonUtilities", - "Projects", - } - ); - } + if (Target.bBuildEditor) + { + PublicDependencyModuleNames.Add("UnrealEd"); + } + + bool bLocalizationModuleAvailable = !Target.bIsEngineInstalled || Target.Configuration != UnrealTargetConfiguration.Shipping; + if (bLocalizationModuleAvailable) + { + PublicDefinitions.Add("WITH_LOCALIZATION_MODULE=1"); + PublicDependencyModuleNames.Add("Localization"); + } + else + { + PublicDefinitions.Add("WITH_LOCALIZATION_MODULE=0"); + } + } } diff --git a/Source/TolgeeEditor/Private/STolgeeSyncDialog.cpp b/Source/TolgeeEditor/Private/STolgeeSyncDialog.cpp deleted file mode 100644 index 3fecd7f..0000000 --- a/Source/TolgeeEditor/Private/STolgeeSyncDialog.cpp +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#include "STolgeeSyncDialog.h" - -#include -#include -#include - -void STolgeeSyncDialog::Construct(const FArguments& InArgs, int NumToUpload, int NumToUpdate, int NumToDelete) -{ - UploadNew.Title = INVTEXT("Upload new"); - UploadNew.NumberOfKeys = NumToUpload; - - UpdateOutdated.Title = INVTEXT("Update outdated"); - UpdateOutdated.NumberOfKeys = NumToUpdate; - - DeleteUnused.Title = INVTEXT("Delete unused"); - DeleteUnused.NumberOfKeys = NumToDelete; - - SWindow::Construct(SWindow::FArguments() - .Title(INVTEXT("Tolgee Sync")) - .SizingRule(ESizingRule::Autosized) - .SupportsMaximize(false) - .SupportsMinimize(false) - [ - SNew(SBorder) - .Padding(4.f) - .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - [ - MakeOperationCheckBox(UploadNew) - ] - - + SVerticalBox::Slot() - [ - MakeOperationCheckBox(UpdateOutdated) - ] - - + SVerticalBox::Slot() - [ - MakeOperationCheckBox(DeleteUnused) - ] - - + SVerticalBox::Slot() - [ - SNew(SSpacer) - ] - - + SVerticalBox::Slot() - [ - SNew(STextBlock) - .Text(this, &STolgeeSyncDialog::GetOperationsSummary) - ] - - + SVerticalBox::Slot() - .HAlign(HAlign_Right) - .VAlign(VAlign_Bottom) - .Padding(8) - [ - SNew(SUniformGridPanel) - .SlotPadding(FAppStyle::GetMargin("StandardDialog.SlotPadding")) - + SUniformGridPanel::Slot(0, 0) - [ - SAssignNew(RunButton, SButton) - .Text(INVTEXT("Run")) - .OnClicked(this, &STolgeeSyncDialog::OnRunClicked) - ] - + SUniformGridPanel::Slot(1, 0) - [ - SNew(SButton) - .Text(INVTEXT("Cancel")) - .HAlign(HAlign_Center) - .OnClicked(this, &STolgeeSyncDialog::OnCancelClicked) - ] - ] - ] - ]); - - RefreshRunButton(); -} - -void STolgeeSyncDialog::RefreshRunButton() -{ - constexpr float InputFocusThickness = 1.0f; - static FButtonStyle BaseButton = FAppStyle::Get().GetWidgetStyle("Button"); - - static FButtonStyle UploadButton = BaseButton - .SetNormal(FSlateRoundedBoxBrush(FStyleColors::Primary, 4.0f, FStyleColors::Input, InputFocusThickness)) - .SetHovered(FSlateRoundedBoxBrush(FStyleColors::PrimaryHover, 4.0f, FStyleColors::Input, InputFocusThickness)) - .SetPressed(FSlateRoundedBoxBrush(FStyleColors::PrimaryPress, 4.0f, FStyleColors::Input, InputFocusThickness)); - - - static FButtonStyle DeleteButton = BaseButton - .SetNormal(FSlateRoundedBoxBrush(COLOR("#E00000FF"), 4.0f, FStyleColors::Input, InputFocusThickness)) - .SetHovered(FSlateRoundedBoxBrush(COLOR("FF0F0EFF"), 4.0f, FStyleColors::Input, InputFocusThickness)) - .SetPressed(FSlateRoundedBoxBrush(COLOR("a00000"), 4.0f, FStyleColors::Input, InputFocusThickness)); - - static FButtonStyle WarningButton = BaseButton - .SetNormal(FSlateRoundedBoxBrush(COLOR("#E07000FF"), 4.0f, FStyleColors::Input, InputFocusThickness)) - .SetHovered(FSlateRoundedBoxBrush(COLOR("#FF870EFF"), 4.0f, FStyleColors::Input, InputFocusThickness)) - .SetPressed(FSlateRoundedBoxBrush(COLOR("#A05000FF"), 4.0f, FStyleColors::Input, InputFocusThickness)); - - if (DeleteUnused.bPerform) - { - RunButton->SetEnabled(true); - RunButton->SetButtonStyle(&DeleteButton); - } - else if (UpdateOutdated.bPerform) - { - RunButton->SetEnabled(true); - RunButton->SetButtonStyle(&WarningButton); - } - else if (UploadNew.bPerform) - { - RunButton->SetEnabled(true); - RunButton->SetButtonStyle(&UploadButton); - } - else - { - RunButton->SetEnabled(false); - RunButton->SetButtonStyle(&BaseButton); - } -} - -FReply STolgeeSyncDialog::OnRunClicked() -{ - RequestDestroyWindow(); - - return FReply::Handled(); -} - -FReply STolgeeSyncDialog::OnCancelClicked() -{ - UploadNew.bPerform = false; - UpdateOutdated.bPerform = false; - DeleteUnused.bPerform = false; - - RequestDestroyWindow(); - - return FReply::Handled(); -} - -FText STolgeeSyncDialog::GetOperationsSummary() const -{ - return FText::Format(INVTEXT("{0} operations will be performed"), GetNumberOfKeysAffectedByOperations()); -} - -int STolgeeSyncDialog::GetNumberOfKeysAffectedByOperations() const -{ - int Total = 0; - if (UploadNew.bPerform) - { - Total += UploadNew.NumberOfKeys; - } - if (UpdateOutdated.bPerform) - { - Total += UpdateOutdated.NumberOfKeys; - } - if (DeleteUnused.bPerform) - { - Total += DeleteUnused.NumberOfKeys; - } - - return Total; -} - -TSharedRef STolgeeSyncDialog::MakeOperationCheckBox(FTolgeeOperation& Operation) -{ - return SNew(SCheckBox) - .IsChecked(this, &STolgeeSyncDialog::IsOperationChecked, &Operation) - .IsEnabled(this, &STolgeeSyncDialog::IsOperationEnabled, &Operation) - .OnCheckStateChanged(this, &STolgeeSyncDialog::OnOperationStateChanged, &Operation) - [ - SNew(STextBlock) - .Text(this, &STolgeeSyncDialog::GetOperationName, &Operation) - ]; -} - -void STolgeeSyncDialog::OnOperationStateChanged(ECheckBoxState NewCheckedState, FTolgeeOperation* Operation) -{ - Operation->bPerform = NewCheckedState == ECheckBoxState::Checked; - - RefreshRunButton(); -} - -ECheckBoxState STolgeeSyncDialog::IsOperationChecked(FTolgeeOperation* Operation) const -{ - return Operation->bPerform ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; -} - -bool STolgeeSyncDialog::IsOperationEnabled(FTolgeeOperation* Operation) const -{ - return Operation->NumberOfKeys > 0; -} - -FText STolgeeSyncDialog::GetOperationName(FTolgeeOperation* Operation) const -{ - return FText::Format(INVTEXT("{0}({1})"), Operation->Title, Operation->NumberOfKeys); - -} \ No newline at end of file diff --git a/Source/TolgeeEditor/Private/STolgeeSyncDialog.h b/Source/TolgeeEditor/Private/STolgeeSyncDialog.h deleted file mode 100644 index 9e15a62..0000000 --- a/Source/TolgeeEditor/Private/STolgeeSyncDialog.h +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#pragma once - -/** - * @brief Represents details about a possible operation (Upload, Update, Delete) - */ -struct FTolgeeOperation -{ - /** - * Name of the operation displayed to the developer - */ - FText Title; - /** - * Number of keys this operation will be affected by running this operation - */ - int NumberOfKeys = 0; - /** - * Should we perform this operation after the dialog closes? - */ - bool bPerform = false; -}; - -/** - * Window used to display the Sync dialog to chose what operations should be performed - */ -class STolgeeSyncDialog : public SWindow -{ -public: - SLATE_BEGIN_ARGS(STolgeeSyncDialog) {} - SLATE_END_ARGS() - - /** - * @brief Constructs the sync operations widget - */ - void Construct(const FArguments& InArgs, int NumToUpload, int NumToUpdate, int NumToDelete); - /** - * Refreshes the Run button style based on the operation that will be executed - */ - void RefreshRunButton(); - /** - * Callback executed when the Run button is clicked - */ - FReply OnRunClicked(); - /** - * Callback executed when the Cancel button is clicked - */ - FReply OnCancelClicked(); - /** - * Constructs a text explaining all the operations that will be performed if the currently selected operations will be run - */ - FText GetOperationsSummary() const; - /** - * Calculates the total number of keys that will be affected if the currently selected operations will be run - */ - int GetNumberOfKeysAffectedByOperations() const; - /** - * Constructs a user-facing checkbox to enable/disable an operation - */ - TSharedRef MakeOperationCheckBox(FTolgeeOperation& Operation); - /** - * Callback executed when a new checkbox state is set for an operation - */ - void OnOperationStateChanged(ECheckBoxState NewCheckedState, FTolgeeOperation* Operation); - /** - * Callback executed to determine the state of a checkbox for an operation - */ - ECheckBoxState IsOperationChecked(FTolgeeOperation* Operation) const; - /** - * Callback executed to determine if a checkbox is enabled for an operation - */ - bool IsOperationEnabled(FTolgeeOperation* Operation) const; - /** - * Callback executed to determine a checkbox title for an operation - */ - FText GetOperationName(FTolgeeOperation* Operation) const; - /** - * Reference to the used to Run the selected operations - */ - TSharedPtr RunButton; - /** - * State of the UploadNew operation - */ - FTolgeeOperation UploadNew; - /** - * State of the UpdateOutdated operation - */ - FTolgeeOperation UpdateOutdated; - /** - * State of the DeleteUnused operation - */ - FTolgeeOperation DeleteUnused; -}; \ No newline at end of file diff --git a/Source/TolgeeEditor/Private/STolgeeTranslationTab.cpp b/Source/TolgeeEditor/Private/STolgeeTranslationTab.cpp index 4b5873c..685cc76 100644 --- a/Source/TolgeeEditor/Private/STolgeeTranslationTab.cpp +++ b/Source/TolgeeEditor/Private/STolgeeTranslationTab.cpp @@ -1,14 +1,18 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #include "STolgeeTranslationTab.h" #include -#include -#include #include +#include +#include +#include #include +#include +#include -#include "TolgeeLocalizationSubsystem.h" +#include "TolgeeEditorIntegrationSubsystem.h" +#include "TolgeeEditorSettings.h" #include "TolgeeLog.h" #include "TolgeeUtils.h" @@ -19,14 +23,18 @@ namespace void STolgeeTranslationTab::Construct(const FArguments& InArgs) { - ActiveTab(); + const UTolgeeEditorSettings* Settings = GetDefault(); + const FString LoginUrl = FString::Printf(TEXT("%s/login"), *Settings->ApiUrl); + + DrawHandle = UDebugDrawService::Register(TEXT("Game"), FDebugDrawDelegate::CreateSP(this, &STolgeeTranslationTab::DebugDrawCallback)); + // clang-format off SDockTab::Construct( SDockTab::FArguments() .TabRole(NomadTab) .OnTabClosed_Raw(this, &STolgeeTranslationTab::CloseTab) [ SAssignNew(Browser, SWebBrowser) - .InitialURL(TolgeeUtils::GetUrlEndpoint(TEXT("login"))) + .InitialURL(LoginUrl) .ShowControls(false) .ShowErrorMessage(true) ]); @@ -35,11 +43,6 @@ void STolgeeTranslationTab::Construct(const FArguments& InArgs) FGlobalTabmanager::Get()->OnActiveTabChanged_Subscribe(FOnActiveTabChanged::FDelegate::CreateSP(this, &STolgeeTranslationTab::OnActiveTabChanged)); } -void STolgeeTranslationTab::ActiveTab() -{ - DrawHandle = UDebugDrawService::Register(TEXT("Game"), FDebugDrawDelegate::CreateSP(this, &STolgeeTranslationTab::DebugDrawCallback)); -} - void STolgeeTranslationTab::CloseTab(TSharedRef DockTab) { UDebugDrawService::Unregister(DrawHandle); @@ -49,19 +52,19 @@ void STolgeeTranslationTab::OnActiveTabChanged(TSharedPtr PreviouslyAc { if (PreviouslyActive == AsShared()) { - GEngine->GetEngineSubsystem()->ManualFetch(); + GEngine->GetEngineSubsystem()->ManualFetch(); } } void STolgeeTranslationTab::DebugDrawCallback(UCanvas* Canvas, APlayerController* PC) { - if(!GEngine->GameViewport) + if (!GEngine->GameViewport) { return; } TSharedPtr GameViewportWidget = GEngine->GameViewport->GetGameViewportWidget(); - if(!GameViewportWidget.IsValid()) + if (!GameViewportWidget.IsValid()) { return; } @@ -89,8 +92,8 @@ void STolgeeTranslationTab::DebugDrawCallback(UCanvas* Canvas, APlayerController // TODO: make this a setting const FVector2D Padding = FVector2D{0.2f, 0.2f}; - const FVector2D UpperLeft = { 0, 0 }; - const FVector2D LowerRight = { 1, 1 }; + const FVector2D UpperLeft = {0, 0}; + const FVector2D LowerRight = {1, 1}; FVector2D Start = HoveredGeometry.GetAbsolutePositionAtCoordinates(UpperLeft) - ViewportGeometry.GetAbsolutePositionAtCoordinates(UpperLeft) + Padding; FVector2D End = HoveredGeometry.GetAbsolutePositionAtCoordinates(LowerRight) - ViewportGeometry.GetAbsolutePositionAtCoordinates(UpperLeft) - Padding; @@ -105,17 +108,110 @@ void STolgeeTranslationTab::DebugDrawCallback(UCanvas* Canvas, APlayerController const TOptional Namespace = FTextInspector::GetNamespace(CurrentText); const TOptional Key = FTextInspector::GetKey(CurrentText); - // Update the browser widget if the current state allows - - const FString EndPoint = FString::Printf(TEXT("translations/single?key=%s&ns=%s"), *Key.Get(""), *Namespace.Get("")); - const FString NewUrl = TolgeeUtils::GetProjectUrlEndpoint(EndPoint); - const FString CurrentUrl = Browser->GetUrl(); - if (NewUrl != CurrentUrl && Browser->IsLoaded()) + // Update the browser widget if we have valid data and the URL is different + if (Namespace && Key) { - UE_LOG(LogTolgee, Log, TEXT("CurrentWidget: %s Namespace: %s Key: %s"), *CurrentTextBlock->GetText().ToString(), *Namespace.Get(""), *Key.Get("")); + const FString CleanNamespace = TextNamespaceUtil::StripPackageNamespace(Namespace.GetValue()); - Browser->LoadURL(NewUrl); + // NOTE: This might look odd, but we need to mirror the id's used internally by Unreal as those are used for importing the key. + const FString TolgeeKeyId = FPlatformHttp::UrlEncode(FString::Printf(TEXT("%s,%s"), *CleanNamespace, *Key.GetValue())); + + ShowWidgetForAsync(TolgeeKeyId); } } } } + +void STolgeeTranslationTab::ShowWidgetForAsync(const FString& TolgeeKeyId) +{ + AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, + [this, TolgeeKeyId]() + { + ShowWidgetFor(TolgeeKeyId); + }); +} + +void STolgeeTranslationTab::ShowWidgetFor(const FString& TolgeeKeyId) +{ + if (bRequestInProgress) + { + return; + } + + TGuardValue ScopedRequestProgress(bRequestInProgress, true); + + const FString ProjectId = FindProjectIdFor(TolgeeKeyId); + if (ProjectId.IsEmpty()) + { + UE_LOG(LogTolgee, Warning, TEXT("No project found for key '%s'"), *TolgeeKeyId); + return; + } + + const UTolgeeEditorSettings* Settings = GetDefault(); + + const FString NewUrl = FString::Printf(TEXT("%s/projects/%s/translations/single?key=%s"), *Settings->ApiUrl, *ProjectId, *TolgeeKeyId); + const FString CurrentUrl = Browser->GetUrl(); + + if (NewUrl != CurrentUrl && Browser->IsLoaded()) + { + UE_LOG(LogTolgee, Log, TEXT("CurrentWidget displayed from %s"), *NewUrl); + + Browser->LoadURL(NewUrl); + } +} + +FString STolgeeTranslationTab::FindProjectIdFor(const FString& TolgeeKeyId) const +{ + const UTolgeeEditorSettings* Settings = GetDefault(); + + TMap PendingRequests; + for (const FString& ProjectId : Settings->ProjectIds) + { + const FString RequestUrl = FString::Printf(TEXT("%s/v2/projects/%s/translations?filterKeyName=%s"), *Settings->ApiUrl, *ProjectId, *TolgeeKeyId); + + FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); + HttpRequest->SetVerb("GET"); + HttpRequest->SetURL(RequestUrl); + HttpRequest->SetHeader(TEXT("X-API-Key"), Settings->ApiKey); + TolgeeUtils::AddSdkHeaders(HttpRequest); + + HttpRequest->ProcessRequest(); + + PendingRequests.Add(ProjectId, HttpRequest); + } + + while (!PendingRequests.IsEmpty()) + { + FPlatformProcess::Sleep(0.1f); + + for (auto RequestIt = PendingRequests.CreateIterator(); RequestIt; ++RequestIt) + { + const TPair Pair = *RequestIt; + const FHttpRequestPtr Request = Pair.Value; + const FString& ProjectId = Pair.Key; + + if (EHttpRequestStatus::IsFinished(Request->GetStatus())) + { + const FHttpResponsePtr Response = Request->GetResponse(); + const FString ResponseContent = Response.IsValid() ? Response->GetContentAsString() : FString(); + + const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(ResponseContent); + TSharedPtr JsonObject; + + if (FJsonSerializer::Deserialize(JsonReader, JsonObject)) + { + const TSharedPtr Embedded = JsonObject->GetObjectField(TEXT("_embedded")); + const TArray> Keys = Embedded->GetArrayField(TEXT("keys")); + if (!Keys.IsEmpty()) + { + return ProjectId; + } + } + + RequestIt.RemoveCurrent(); + } + } + } + + return {}; +} \ No newline at end of file diff --git a/Source/TolgeeEditor/Private/TolgeeEditor.cpp b/Source/TolgeeEditor/Private/TolgeeEditor.cpp index e628360..42188f2 100644 --- a/Source/TolgeeEditor/Private/TolgeeEditor.cpp +++ b/Source/TolgeeEditor/Private/TolgeeEditor.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #include "TolgeeEditor.h" @@ -6,11 +6,8 @@ #include #include "STolgeeTranslationTab.h" -#include "TolgeeEditorIntegrationSubsystem.h" -#include "TolgeeLocalizationSubsystem.h" -#include "TolgeeSettings.h" +#include "TolgeeEditorSettings.h" #include "TolgeeStyle.h" -#include "TolgeeUtils.h" #define LOCTEXT_NAMESPACE "Tolgee" @@ -19,20 +16,6 @@ namespace const FName MenuTabName = FName("TolgeeDashboardMenuTab"); } -FTolgeeEditorModule& FTolgeeEditorModule::Get() -{ - static const FName TolgeeEditorModuleName = "TolgeeEditor"; - return FModuleManager::LoadModuleChecked(TolgeeEditorModuleName); -} - -void FTolgeeEditorModule::ActivateWindowTab() -{ - if (FGlobalTabmanager::Get()->HasTabSpawner(MenuTabName)) - { - FGlobalTabmanager::Get()->TryInvokeTab(MenuTabName); - } -} - void FTolgeeEditorModule::StartupModule() { RegisterStyle(); @@ -72,7 +55,7 @@ void FTolgeeEditorModule::RegisterWindowExtension() // clang-format off DashboardTab.SetDisplayName(LOCTEXT("DashboardName", "Tolgee Dashboard")) .SetTooltipText(LOCTEXT("DashboardTooltip", "Get an overview of your project state in a separate tab.")) - .SetIcon(FSlateIcon(FTolgeeStyle::GetStyleSetName(), "Tolgee.Settings")) + .SetIcon(FSlateIcon(FTolgeeStyle::Get().GetStyleSetName(), "Tolgee.Settings")) .SetMenuType(ETabSpawnerMenuType::Hidden); // clang-format on } @@ -120,27 +103,30 @@ void FTolgeeEditorModule::ExtendToolbar(FToolBarBuilder& Builder) FMenuBuilder MenuBuilder(true, NULL); MenuBuilder.AddMenuEntry( - LOCTEXT("WebDashboard", "Web dashboard"), - LOCTEXT("WebDashboardTip", "Launches the Tolgee dashboard in your browser"), + LOCTEXT("TranslationDashboard", "Translation Tab"), + LOCTEXT("TranslationDashboardTip", "Open a translation widget inside Unreal which allows you translate text by hovering on top"), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda( []() { - FPlatformProcess::LaunchURL(*TolgeeUtils::GetProjectUrlEndpoint(), nullptr, nullptr); + FGlobalTabmanager::Get()->TryInvokeTab(MenuTabName); } )) ); + MenuBuilder.AddMenuEntry( - LOCTEXT("TranslationDashboard", "Translation Dashboard"), - LOCTEXT("TranslationDashboardTip", "Open a translation widget inside Unreal which allows you translate text by hovering on top"), + LOCTEXT("WebDashboard", "Web dashboard"), + LOCTEXT("WebDashboardTip", "Launches the Tolgee dashboard in your browser"), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda( []() { - FGlobalTabmanager::Get()->TryInvokeTab(MenuTabName); + const UTolgeeEditorSettings* Settings = GetDefault(); + FPlatformProcess::LaunchURL(*Settings->ApiUrl, nullptr, nullptr); } )) ); + MenuBuilder.AddMenuEntry( LOCTEXT("OpenSettings", "Open Settings"), LOCTEXT("OpenSettingsTip", "Open the plugin settings section"), @@ -148,10 +134,12 @@ void FTolgeeEditorModule::ExtendToolbar(FToolBarBuilder& Builder) FUIAction(FExecuteAction::CreateLambda( []() { - UTolgeeSettings::OpenSettings(); + //TODO: Enable this after settings get unified + //UTolgeeSettings::OpenSettings(); } )) ); + MenuBuilder.AddMenuEntry( LOCTEXT("OpenDocumentation", "Open Documentation"), LOCTEXT("OpenDocumentationTip", "Open a step by step guide on how to use our integration"), @@ -165,6 +153,7 @@ void FTolgeeEditorModule::ExtendToolbar(FToolBarBuilder& Builder) } )) ); + MenuBuilder.AddMenuEntry( LOCTEXT("GetSupport", "Get support"), LOCTEXT("GetSupportTip", "Join our slack and get support directly"), @@ -178,6 +167,7 @@ void FTolgeeEditorModule::ExtendToolbar(FToolBarBuilder& Builder) } )) ); + MenuBuilder.AddMenuEntry( LOCTEXT("ReportIssue", "Report issue"), LOCTEXT("ReportIssueTip", "Report an issue on our GitHub"), @@ -190,36 +180,12 @@ void FTolgeeEditorModule::ExtendToolbar(FToolBarBuilder& Builder) )) ); - MenuBuilder.AddMenuEntry( - LOCTEXT("Syncronize", "Syncronize"), - LOCTEXT("SyncronizeTip", "Syncronizes the state between local state and Tolgee backend, reconciling any differences"), - FSlateIcon(), - FUIAction(FExecuteAction::CreateLambda( - []() - { - GEditor->GetEditorSubsystem()->Sync(); - } - )) - ); - - MenuBuilder.AddMenuEntry( - LOCTEXT("DownloadTranslations", "Download Translations.json"), - LOCTEXT("DownloadTranslationsTip", "Download the latest Translations.json file from Tolgee and add it to VCS"), - FSlateIcon(), - FUIAction(FExecuteAction::CreateLambda( - []() - { - GEditor->GetEditorSubsystem()->DownloadTranslationsJson(); - } - )) - ); - return MenuBuilder.MakeWidget(); } ), LOCTEXT("TolgeeDashboardSettingsCombo", "Tolgee Settings"), LOCTEXT("TolgeeDashboardSettingsCombo_ToolTip", "Tolgee Dashboard settings"), - FSlateIcon(FTolgeeStyle::GetStyleSetName(), "Tolgee.Settings"), + FSlateIcon(FTolgeeStyle::Get().GetStyleSetName(), "Tolgee.Settings"), false ); } diff --git a/Source/TolgeeEditor/Private/TolgeeEditorIntegrationSubsystem.cpp b/Source/TolgeeEditor/Private/TolgeeEditorIntegrationSubsystem.cpp index 69eb5d4..550e0cb 100644 --- a/Source/TolgeeEditor/Private/TolgeeEditorIntegrationSubsystem.cpp +++ b/Source/TolgeeEditor/Private/TolgeeEditorIntegrationSubsystem.cpp @@ -1,658 +1,240 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #include "TolgeeEditorIntegrationSubsystem.h" -#include #include -#include -#include -#include -#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include - -#include "STolgeeSyncDialog.h" -#include "TolgeeEditor.h" -#include "TolgeeLocalizationSubsystem.h" +#include + +#include "TolgeeEditorSettings.h" #include "TolgeeLog.h" -#include "TolgeeSettings.h" +#include "TolgeeRuntimeSettings.h" #include "TolgeeUtils.h" -#define LOCTEXT_NAMESPACE "Tolgee" - -namespace +void UTolgeeEditorIntegrationSubsystem::ManualFetch() { - bool IsSameKey(const FLocalizedKey& A, const FLocalizationKey& B) - { - return A.Name == B.Key && A.Namespace == B.Namespace && A.Hash == TolgeeUtils::GetTranslationHash(B.DefaultText); - } - - bool IsSameKeyWithChangedHash(const FLocalizedKey& A, const FLocalizationKey& B) - { - return A.Name == B.Key && A.Namespace == B.Namespace && A.Hash != TolgeeUtils::GetTranslationHash(B.DefaultText); - } - - bool IsSameKey(const FLocalizationKey& B, const FLocalizedKey& A) - { - return IsSameKey(A, B); - } -} // namespace - -template -TArray RemoveExisting(const TArray& InArray, const TArray& InExisting) -{ - TArray Result; - for (const T& InElement : InArray) - { - const bool bExists = InExisting.ContainsByPredicate([InElement](const U& Element) - { - return IsSameKey(InElement, Element); - }); - - if (!bExists) - { - Result.Add(InElement); - } - } - - return Result; + FetchIUpdatesAreAvailableAsync(); } -void UTolgeeEditorIntegrationSubsystem::Sync() +void UTolgeeEditorIntegrationSubsystem::OnGameInstanceStart(UGameInstance* GameInstance) { - // Gather local keys - TValueOrError, FText> LocalKeysResult = GatherLocalKeys(); - if (LocalKeysResult.HasError()) + const UTolgeeRuntimeSettings* RuntimeSettings = GetDefault(); + if (RuntimeSettings->bUseCdnInEditor) { - FMessageDialog::Open(EAppMsgType::Ok, INVTEXT("Gathering local keys failed.")); + UE_LOG(LogTolgee, Display, TEXT("Tolgee is configured to use CDN in editor, fetching directly from Tolgee dashboard is disabled. If you want to use the dashboard data directly, disable CDN in the Tolgee settings.")); return; } - const TArray LocalKeys = LocalKeysResult.GetValue(); - - // Gather remote keys - UTolgeeLocalizationSubsystem* LocalizationSubsystem = GEngine->GetEngineSubsystem(); - LocalizationSubsystem->ManualFetch(); - - // We need to wait synchronously wait for the fetch response to ensure we have the latest data - while (LocalizationSubsystem->IsFetchInprogress()) - { - FPlatformProcess::Sleep(0.01f); - - if (IsInGameThread()) - { - FHttpModule::Get().GetHttpManager().Tick(0.01f); - } - } - - const TArray RemoteKeys = LocalizationSubsystem->GetLocalizedDictionary().Keys; - - TArray KeysToAdd = RemoveExisting(LocalKeys, RemoteKeys); - TArray KeysToRemove = RemoveExisting(RemoteKeys, LocalKeys); - - TArray> KeysToUpdate; - - for (auto KeyToAddIt = KeysToAdd.CreateIterator(); KeyToAddIt; ++KeyToAddIt) - { - int32 KeyToRemoveIndex = KeysToRemove.IndexOfByPredicate([KeyToAdd = *KeyToAddIt](const FLocalizedKey& Element) - { - return IsSameKeyWithChangedHash(Element, KeyToAdd); - }); - - if (KeyToRemoveIndex != INDEX_NONE) - { - KeysToUpdate.Emplace(*KeyToAddIt, KeysToRemove[KeyToRemoveIndex]); - - KeyToAddIt.RemoveCurrent(); - KeysToRemove.RemoveAt(KeyToRemoveIndex); - } - } - - if (KeysToAdd.IsEmpty() && KeysToUpdate.IsEmpty() && KeysToRemove.IsEmpty()) + const UTolgeeEditorSettings* EditorSettings = GetDefault(); + if (EditorSettings->ProjectIds.IsEmpty()) { - FMessageDialog::Open(EAppMsgType::Ok, INVTEXT("Everything is up to date.")); + UE_LOG(LogTolgee, Display, TEXT("CDN was disabled in editor, but no projects are configured. Static data will be used instead.")); return; } - TSharedRef SyncWindow = SNew(STolgeeSyncDialog, KeysToAdd.Num(), KeysToUpdate.Num(), KeysToRemove.Num()); + FTimerDelegate Delegate = FTimerDelegate::CreateUObject(this, &ThisClass::OnRefreshTick); + GameInstance->GetWorld()->GetTimerManager().SetTimer(RefreshTick, Delegate, EditorSettings->RefreshInterval, true); - GEditor->EditorAddModalWindow(SyncWindow); - - const bool bRunUpload = SyncWindow->UploadNew.bPerform; - const bool bRunUpdate = SyncWindow->UpdateOutdated.bPerform; - const bool bRunDelete = SyncWindow->DeleteUnused.bPerform; - - if (bRunUpload) - { - UploadLocalKeys(KeysToAdd); - } - - if (bRunUpdate) - { - UpdateOutdatedKeys(KeysToUpdate); - } - - if (bRunDelete) - { - DeleteRemoteKeys(KeysToRemove); - } + FetchAllProjects(); } -void UTolgeeEditorIntegrationSubsystem::DownloadTranslationsJson() +void UTolgeeEditorIntegrationSubsystem::OnGameInstanceEnd(bool bIsSimulating) { - // Gather remote keys - UTolgeeLocalizationSubsystem* LocalizationSubsystem = GEngine->GetEngineSubsystem(); - LocalizationSubsystem->ManualFetch(); - - // We need to wait synchronously wait for the fetch response to ensure we have the latest data - while (LocalizationSubsystem->IsFetchInprogress()) - { - FPlatformProcess::Sleep(0.01f); - - if (IsInGameThread()) - { - FHttpModule::Get().GetHttpManager().Tick(0.01f); - } - } - - const FString FilePath = TolgeeUtils::GetLocalizationSourceFile(); - - EnsureFileCheckedOutSourceControl(FilePath); - bool bWasModified = ExportLocalTranslations(); - EnsureAddedStateSourceControl(FilePath, bWasModified); + ResetData(); } -bool UTolgeeEditorIntegrationSubsystem::EnsureFileCheckedOutSourceControl(FString FilePath) +TMap> UTolgeeEditorIntegrationSubsystem::GetDataToInject() const { - if (!FPaths::FileExists(FilePath)) - { - return true; - } - - ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); - - if (!SourceControlProvider.IsEnabled()) - { - return true; - } - - FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(FilePath, EStateCacheUsage::ForceUpdate); - if (!SourceControlState.IsValid() || !SourceControlState->IsSourceControlled()) - { - return true; - } - - if (!SourceControlState->IsCheckedOut()) - { - TSharedRef CheckOutOperation = ISourceControlOperation::Create(); - - if (SourceControlProvider.Execute(CheckOutOperation, FilePath) == ECommandResult::Succeeded) - { - return true; - } - - return false; - } - - return true; + return CachedTranslations; } -bool UTolgeeEditorIntegrationSubsystem::EnsureAddedStateSourceControl(FString FilePath, bool bWasFileModified) +void UTolgeeEditorIntegrationSubsystem::FetchAllProjects() { - if (!FPaths::FileExists(FilePath)) - { - return false; - } - - ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); - - if (!SourceControlProvider.IsEnabled()) - { - return true; - } - - // Ensure we have the latest state from Perforce - TArray FilesToUpdate = { FilePath }; - SourceControlProvider.Execute(ISourceControlOperation::Create(), FilesToUpdate); - - // Fetch the refreshed state - FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(FilePath, EStateCacheUsage::ForceUpdate); - - if (SourceControlState.IsValid() && SourceControlState->IsSourceControlled() && SourceControlState->IsCheckedOut()) - { - // TODO: The idiomatic way of checking if the file was modified would be to use SourceControlState->IsModified() - // But that did not work in a reliable way with Perforce even when retrying with waiting up to 5 seconds and still the file was - // reverted even if it was modified. Relying on the provided bool bWasFileModified is a workaround to make this reliable. - - if (bWasFileModified) - { - // File is modified, do NOT revert it - return true; - } + const UTolgeeEditorSettings* Settings = GetDefault(); - // If the file is checked out but NOT modified, revert it - TSharedRef RevertOperation = ISourceControlOperation::Create(); - SourceControlProvider.Execute(RevertOperation, FilePath); - - return true; - } - - TSharedRef AddOperation = ISourceControlOperation::Create(); - if (SourceControlProvider.Execute(AddOperation, FilePath) == ECommandResult::Succeeded) + for (const FString& ProjectId : Settings->ProjectIds) { - return true; + const FString RequestUrl = FString::Printf(TEXT("%s/v2/projects/%s/export?format=PO"), *Settings->ApiUrl, *ProjectId); + UE_LOG(LogTolgee, Display, TEXT("Fetching localization data for project %s from Tolgee dashboard: %s"), *ProjectId, *RequestUrl); + FetchFromDashboard(ProjectId, RequestUrl); } - return false; + LastFetchTime = FDateTime::UtcNow(); } -void UTolgeeEditorIntegrationSubsystem::UploadLocalKeys(TArray NewLocalKeys) +void UTolgeeEditorIntegrationSubsystem::FetchFromDashboard(const FString& ProjectId, const FString& RequestUrl) { - const UTolgeeSettings* Settings = GetDefault(); - const FString DefaultLocale = Settings->Languages.Num() > 0 ? Settings->Languages[0] : TEXT("en"); - - TArray> Keys; - - UE_LOG(LogTolgee, Log, TEXT("Upload request payload:")); - for (const auto& Key : NewLocalKeys) - { - UE_LOG(LogTolgee, Log, TEXT("- namespace:%s key:%s default:%s"), *Key.Namespace, *Key.Key, *Key.DefaultText); - - FKeyItemPayload KeyItem; - KeyItem.Name = Key.Key; - KeyItem.Namespace = Key.Namespace; - - const FString HashTag = FString::Printf(TEXT("%s%s"), *TolgeeUtils::KeyHashPrefix, *LexToString(TolgeeUtils::GetTranslationHash(Key.DefaultText))); - KeyItem.Tags.Emplace(HashTag); + const UTolgeeEditorSettings* Settings = GetDefault(); - TSharedPtr KeyObject = FJsonObjectConverter::UStructToJsonObject(KeyItem); + FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); + HttpRequest->SetURL(RequestUrl); + HttpRequest->SetVerb("GET"); + HttpRequest->SetHeader(TEXT("X-API-Key"), Settings->ApiKey); + TolgeeUtils::AddSdkHeaders(HttpRequest); - TSharedRef TranslationObject = MakeShareable(new FJsonObject); - TranslationObject->SetStringField(DefaultLocale, Key.DefaultText); - KeyObject->SetObjectField(TEXT("translations"), TranslationObject); - - Keys.Add(MakeShared(KeyObject)); - } - - TSharedRef PayloadJson = MakeShared(); - PayloadJson->SetArrayField(TEXT("keys"), Keys); - - FString Json; - TSharedRef> Writer = TJsonWriterFactory<>::Create(&Json); - FJsonSerializer::Serialize(PayloadJson, Writer); - - const FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); - HttpRequest->SetURL(TolgeeUtils::GetUrlEndpoint(TEXT("v2/projects/keys/import"))); - HttpRequest->SetVerb("POST"); - HttpRequest->SetHeader(TEXT("X-API-Key"), GetDefault()->ApiKey); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Type"), TolgeeUtils::GetSdkType()); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Version"), TolgeeUtils::GetSdkVersion()); - HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - HttpRequest->SetContentAsString(Json); - HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnLocalKeysUploaded); + HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnFetchedFromDashboard, ProjectId); HttpRequest->ProcessRequest(); -} - -void UTolgeeEditorIntegrationSubsystem::OnLocalKeysUploaded(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) -{ - UE_LOG(LogTolgee, Verbose, TEXT("UTolgeeEditorIntegrationSubsystem::OnLocalKeysUploaded")); - - if (!bWasSuccessful) - { - UE_LOG(LogTolgee, Error, TEXT("Request to upload new keys was unsuccessful.")); - return; - } - if (!EHttpResponseCodes::IsOk(Response->GetResponseCode())) - { - UE_LOG(LogTolgee, Error, TEXT("Request to upload new keys received unexpected code: %s content: %s"), *LexToString(Response->GetResponseCode()), *Response->GetContentAsString()); - return; - } - - UE_LOG(LogTolgee, Log, TEXT("New keys uploaded successfully.")); - GEngine->GetEngineSubsystem()->ManualFetch(); + NumRequestsSent++; } -void UTolgeeEditorIntegrationSubsystem::DeleteRemoteKeys(TArray UnusedRemoteKeys) +void UTolgeeEditorIntegrationSubsystem::OnFetchedFromDashboard(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString ProjectId) { - UE_LOG(LogTolgee, Log, TEXT("Delete request payload:")); - FKeysDeletePayload Payload; - for (const auto& Key : UnusedRemoteKeys) + if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) { - if (Key.RemoteId.IsSet()) - { - UE_LOG(LogTolgee, Log, TEXT("- id:%s namespace:%s key:%s"), *LexToString(Key.RemoteId.GetValue()), *Key.Namespace, *Key.Name); - Payload.Ids.Add(Key.RemoteId.GetValue()); - } - else - { - UE_LOG(LogTolgee, Warning, TEXT("- namespace:%s key:%s -> Cannot be deleted: Invalid id."), *Key.Namespace, *Key.Name); - } + UE_LOG(LogTolgee, Display, TEXT("Fetch successfully for %s to %s."), *ProjectId, *Request->GetURL()); + ReadTranslationsFromZipContent(ProjectId, Response->GetContent()); } - - FString Json; - FJsonObjectConverter::UStructToJsonObjectString(Payload, Json); - - const FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); - HttpRequest->SetURL(TolgeeUtils::GetUrlEndpoint(TEXT("v2/projects/keys"))); - HttpRequest->SetVerb("DELETE"); - HttpRequest->SetHeader(TEXT("X-API-Key"), GetDefault()->ApiKey); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Type"), TolgeeUtils::GetSdkType()); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Version"), TolgeeUtils::GetSdkVersion()); - HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - HttpRequest->SetContentAsString(Json); - HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnRemoteKeysDeleted); - HttpRequest->ProcessRequest(); -} - -void UTolgeeEditorIntegrationSubsystem::OnRemoteKeysDeleted(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) -{ - UE_LOG(LogTolgee, Verbose, TEXT("UTolgeeEditorIntegrationSubsystem::OnRemoteKeysDeleted")); - - if (!bWasSuccessful) - { - UE_LOG(LogTolgee, Error, TEXT("Request to delete unused keys was unsuccessful.")); - return; - } - if (!EHttpResponseCodes::IsOk(Response->GetResponseCode())) + else { - UE_LOG(LogTolgee, Error, TEXT("Request to delete unused keys received unexpected code: %s content: %s"), *LexToString(Response->GetResponseCode()), *Response->GetContentAsString()); - return; + UE_LOG(LogTolgee, Error, TEXT("Request for %s to %s failed."), *ProjectId, *Request->GetURL()); } - UE_LOG(LogTolgee, Log, TEXT("Unused keys deleted successfully.")); - - GEngine->GetEngineSubsystem()->ManualFetch(); -} + NumRequestsCompleted++; -void UTolgeeEditorIntegrationSubsystem::UpdateOutdatedKeys(TArray> OutdatedKeys) -{ - for (const auto& KeyToUpdate : OutdatedKeys) + if (NumRequestsCompleted == NumRequestsSent) { - UE_LOG(LogTolgee, Log, TEXT("Update request payload:")); - UE_LOG(LogTolgee, Log, TEXT("- id:%s namespace:%s key:%s"), *LexToString(KeyToUpdate.Value.RemoteId.GetValue()), *KeyToUpdate.Value.Namespace, *KeyToUpdate.Value.Name); - - FKeyItemPayload Payload; - Payload.Name = KeyToUpdate.Value.Name; - Payload.Namespace = KeyToUpdate.Value.Namespace; - - const FString HashTag = FString::Printf(TEXT("%s%s"), *TolgeeUtils::KeyHashPrefix, *LexToString(TolgeeUtils::GetTranslationHash(KeyToUpdate.Key.DefaultText))); - Payload.Tags.Add(HashTag); - - FString Json; - FJsonObjectConverter::UStructToJsonObjectString(Payload, Json); - - FString KeyUrl = FString::Printf(TEXT("v2/projects/keys/%lld/complex-update"), KeyToUpdate.Value.RemoteId.GetValue()); - const FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); - HttpRequest->SetURL(TolgeeUtils::GetUrlEndpoint(KeyUrl)); - HttpRequest->SetVerb("PUT"); - HttpRequest->SetHeader(TEXT("X-API-Key"), GetDefault()->ApiKey); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Type"), TolgeeUtils::GetSdkType()); - HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Version"), TolgeeUtils::GetSdkVersion()); - HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - HttpRequest->SetContentAsString(Json); - HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnOutdatedKeyUpdated); - HttpRequest->ProcessRequest(); + UE_LOG(LogTolgee, Display, TEXT("All requests completed. Refreshing translation data.")); + RefreshTranslationDataAsync(); } } -void UTolgeeEditorIntegrationSubsystem::OnOutdatedKeyUpdated(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) +void UTolgeeEditorIntegrationSubsystem::ResetData() { - UE_LOG(LogTolgee, Verbose, TEXT("UTolgeeEditorIntegrationSubsystem::OnOutdatedKeysUpdated")); - - if (!bWasSuccessful) - { - UE_LOG(LogTolgee, Error, TEXT("Request to updated outdated keys was unsuccessful.")); - return; - } - if (!EHttpResponseCodes::IsOk(Response->GetResponseCode())) - { - UE_LOG(LogTolgee, Error, TEXT("Request to updated outdated keys received unexpected code: %s content: %s"), *LexToString(Response->GetResponseCode()), *Response->GetContentAsString()); - return; - } + NumRequestsSent = 0; + NumRequestsCompleted = 0; - UE_LOG(LogTolgee, Log, TEXT("Outdated keys updated successfully.")); - - GEngine->GetEngineSubsystem()->ManualFetch(); + CachedTranslations.Empty(); + LastFetchTime = {0}; } -TValueOrError, FText> UTolgeeEditorIntegrationSubsystem::GatherLocalKeys() const +bool UTolgeeEditorIntegrationSubsystem::ReadTranslationsFromZipContent(const FString& ProjectId, const TArray& ResponseContent) { - TArray TargetObjectsToProcess = GatherValidLocalizationTargets(); - if (TargetObjectsToProcess.Num() == 0) + static FCriticalSection ReadFromFileCriticalSection; + FScopeLock Lock(&ReadFromFileCriticalSection); + + const FString TempPath = FPaths::ProjectSavedDir() / TEXT("Tolgee") / TEXT("In-Context") / ProjectId + TEXT(".zip"); + const bool bSaveSuccess = FFileHelper::SaveArrayToFile(ResponseContent, *TempPath); + if (!bSaveSuccess) { - return MakeError(INVTEXT("No valid TargetObjects found in GetGameTargetSet.")); + UE_LOG(LogTolgee, Error, TEXT("Failed to save zip for project %s as file to %s"), *ProjectId, *TempPath); + return false; } - // Update the localization target before extract the keys out of them - const TSharedPtr ParentWindow = FSlateApplication::Get().GetActiveTopLevelWindow(); - const bool bWasSuccessful = LocalizationCommandletTasks::GatherTextForTargets(ParentWindow.ToSharedRef(), TargetObjectsToProcess); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); - if (!bWasSuccessful) + IFileHandle* ArchiveFileHandle = PlatformFile.OpenRead(*TempPath); + FZipArchiveReader ZipReader = {ArchiveFileHandle}; + if (!ZipReader.IsValid()) { - return MakeError(INVTEXT("GatherTextFromTargets failed to update TargetObjects.")); + UE_LOG(LogTolgee, Error, TEXT("Failed to open zip for project %s located at %s"), *ProjectId, *TempPath); + return false; } - return MakeValue(GetKeysFromTargets(TargetObjectsToProcess)); -} - -TArray UTolgeeEditorIntegrationSubsystem::GetKeysFromTargets(TArray LocalizationTargets) const -{ - TArray KeysFound; - - const UTolgeeSettings* TolgeeSettings = GetDefault(); - - for (const ULocalizationTarget* LocalizationTarget : LocalizationTargets) + const TArray FileNames = ZipReader.GetFileNames(); + for (const FString& FileName : FileNames) { - GWarn->BeginSlowTask(LOCTEXT("LoadingManifestData", "Loading Entries from Current Translation Manifest..."), true); - - const FString ManifestFilePathToRead = LocalizationConfigurationScript::GetManifestPath(LocalizationTarget); - const TSharedRef ManifestAtHeadRevision = MakeShared(); - - if (!FJsonInternationalizationManifestSerializer::DeserializeManifestFromFile(ManifestFilePathToRead, ManifestAtHeadRevision)) + TArray FileBuffer; + if (ZipReader.TryReadFile(FileName, FileBuffer)) { - UE_LOG(LogTemp, Error, TEXT("Could not read manifest file %s."), *ManifestFilePathToRead); - continue; - } + const FString InCulture = FPaths::GetBaseFilename(FileName); + const FString FileContents = FString(FileBuffer.Num(), UTF8_TO_TCHAR(FileBuffer.GetData())); - const int32 TotalManifestEntries = ManifestAtHeadRevision->GetNumEntriesBySourceText(); - if (TotalManifestEntries < 1) - { - UE_LOG(LogTemp, Error, TEXT("Error Loading Translations!")); - continue; + TArray Translations = ExtractTranslationsFromPO(FileContents); + CachedTranslations.FindOrAdd(InCulture).Append(Translations); } - - int32 CurrentManifestEntry = 0; - for (auto ManifestItr = ManifestAtHeadRevision->GetEntriesBySourceTextIterator(); ManifestItr; ++ManifestItr, ++CurrentManifestEntry) + else { - const FText ProgressText = FText::Format(LOCTEXT("LoadingManifestDataProgress", "Loading {0}/{1}"), FText::AsNumber(CurrentManifestEntry), FText::AsNumber(TotalManifestEntries)); - GWarn->StatusUpdate(CurrentManifestEntry, TotalManifestEntries, ProgressText); - - const TSharedRef ManifestEntry = ManifestItr.Value(); - - if (TolgeeSettings->bIgnoreGeneratedKeys && ManifestEntry->Namespace.IsEmpty()) - { - // Only generated keys have no namespace. Keys from string tables are required to have a one. - continue; - } - - for (auto ContextIter(ManifestEntry->Contexts.CreateConstIterator()); ContextIter; ++ContextIter) - { - FLocalizationKey& NewKey = KeysFound.Emplace_GetRef(); - NewKey.Key = ContextIter->Key.GetString(); - NewKey.Namespace = ManifestEntry->Namespace.GetString(); - NewKey.DefaultText = ManifestEntry->Source.Text; - } + UE_LOG(LogTolgee, Warning, TEXT("Failed to read file %s for project %s inside zip at %s"), *FileName, *ProjectId, *TempPath); } - - GWarn->EndSlowTask(); } - return KeysFound; + return true; } -TArray UTolgeeEditorIntegrationSubsystem::GatherValidLocalizationTargets() const +void UTolgeeEditorIntegrationSubsystem::OnRefreshTick() { - TArray Result; - const ULocalizationTargetSet* GameTargetSet = ULocalizationSettings::GetGameTargetSet(); - for (ULocalizationTarget* LocalizationTarget : GameTargetSet->TargetObjects) - { - if (LocalizationTarget) - { - const FLocalizationTargetSettings& LocalizationSettings = LocalizationTarget->Settings; - const bool bValidCulture = LocalizationSettings.SupportedCulturesStatistics.IsValidIndex(LocalizationSettings.NativeCultureIndex); - if (!bValidCulture) - { - UE_LOG(LogTolgee, Warning, TEXT("Skipping: %s -> Invalid default culture"), *LocalizationTarget->Settings.Name); - continue; - } - - FText GatherTextError; - if (LocalizationSettings.GatherFromTextFiles.IsEnabled && !LocalizationSettings.GatherFromTextFiles.Validate(false, GatherTextError)) - { - UE_LOG(LogTolgee, Warning, TEXT("Skipping: %s -> GatherText: %s"), *LocalizationTarget->Settings.Name, *GatherTextError.ToString()); - continue; - } - FText GatherPackageError; - if (LocalizationSettings.GatherFromPackages.IsEnabled && !LocalizationSettings.GatherFromPackages.Validate(false, GatherPackageError)) - { - UE_LOG(LogTolgee, Warning, TEXT("Skipping: %s -> GatherPackage: %s"), *LocalizationTarget->Settings.Name, *GatherPackageError.ToString()); - continue; - } - FText GatherMetadataError; - if (LocalizationSettings.GatherFromMetaData.IsEnabled && !LocalizationSettings.GatherFromMetaData.Validate(false, GatherMetadataError)) - { - UE_LOG(LogTolgee, Warning, TEXT("Skipping: %s -> GatherMetadata: %s"), *LocalizationTarget->Settings.Name, *GatherMetadataError.ToString()); - continue; - } - - Result.Add(LocalizationTarget); - } - } - - return Result; + FetchIUpdatesAreAvailableAsync(); } -void UTolgeeEditorIntegrationSubsystem::OnMainFrameReady(TSharedPtr InRootWindow, bool bIsRunningStartupDialog) +void UTolgeeEditorIntegrationSubsystem::FetchIUpdatesAreAvailableAsync() { - // The browser plugin CEF version is too old to render the Tolgee website before 5.0 -#if UE_VERSION_NEWER_THAN(5, 0, 0) - const UTolgeeSettings* TolgeeSettings = GetDefault(); - - // If the API Key is not set in the settings, the user has not completed the setup, therefore we will our welcome tab. - if (TolgeeSettings && TolgeeSettings->ApiKey.IsEmpty()) - { - FTolgeeEditorModule& TolgeeEditorModule = FTolgeeEditorModule::Get(); - TolgeeEditorModule.ActivateWindowTab(); - } -#endif + AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, + [this]() + { + FetchIfProjectsWereUpdated(); + }); } -bool UTolgeeEditorIntegrationSubsystem::ExportLocalTranslations() +void UTolgeeEditorIntegrationSubsystem::FetchIfProjectsWereUpdated() { - const UTolgeeLocalizationSubsystem* LocalizationSubsystem = GEngine->GetEngineSubsystem(); - while (LocalizationSubsystem->GetLocalizedDictionary().Keys.Num() == 0) + if (bRequestInProgress) { - constexpr float SleepInterval = 0.1; - - UE_LOG(LogTolgee, Display, TEXT("Waiting for translation data. Retrying in %s second."), *LexToString(SleepInterval)); - FPlatformProcess::Sleep(SleepInterval); - FHttpModule::Get().GetHttpManager().Tick(SleepInterval); + return; } - UE_LOG(LogTolgee, Display, TEXT("Got translation data.")); - const FLocalizedDictionary& Dictionary = LocalizationSubsystem->GetLocalizedDictionary(); + TGuardValue ScopedRequestProgress(bRequestInProgress, true); - FString JsonString; - if (!FJsonObjectConverter::UStructToJsonObjectString(Dictionary, JsonString)) - { - UE_LOG(LogTolgee, Error, TEXT("Couldn't convert the localized dictionary to string")); - return false; - } + FDateTime LastProjectUpdate = {0}; + TArray PendingRequests; + const UTolgeeEditorSettings* Settings = GetDefault(); - const FString Filename = TolgeeUtils::GetLocalizationSourceFile(); - FString BeforeHash; - if (FPaths::FileExists(Filename)) + for (const FString& ProjectId : Settings->ProjectIds) { - FString BeforeFileContents; - if (FFileHelper::LoadFileToString(BeforeFileContents, *Filename)) - { - BeforeHash = FMD5::HashAnsiString(*BeforeFileContents); - } - } + const FString RequestUrl = FString::Printf(TEXT("%s/v2/projects/%s/stats"), *Settings->ApiUrl, *ProjectId); - bool wasModified = BeforeHash != FMD5::HashAnsiString(*JsonString); - - if (!FFileHelper::SaveStringToFile(JsonString, *Filename)) - { - UE_LOG(LogTolgee, Error, TEXT("Couldn't save the localized dictionary to file: %s"), *Filename); - return false; - } + FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); + HttpRequest->SetVerb("GET"); + HttpRequest->SetURL(RequestUrl); + HttpRequest->SetHeader(TEXT("X-API-Key"), Settings->ApiKey); + TolgeeUtils::AddSdkHeaders(HttpRequest); - const FDirectoryPath TolgeeLocalizationPath = TolgeeUtils::GetLocalizationDirectory(); - UProjectPackagingSettings* ProjectPackagingSettings = GetMutableDefault(); - const bool bContainsPath = ProjectPackagingSettings->DirectoriesToAlwaysStageAsNonUFS.ContainsByPredicate( - [TolgeeLocalizationPath](const FDirectoryPath& Path) - { - return Path.Path == TolgeeLocalizationPath.Path; - } - ); + HttpRequest->ProcessRequest(); - if (!bContainsPath) - { - ProjectPackagingSettings->DirectoriesToAlwaysStageAsNonUFS.Add(TolgeeLocalizationPath); - ProjectPackagingSettings->SaveConfig(); + PendingRequests.Add(HttpRequest); } - - UE_LOG(LogTolgee, Display, TEXT("Localized dictionary succesfully saved to file: %s"), *Filename); - return wasModified; -} -void UTolgeeEditorIntegrationSubsystem::Initialize(FSubsystemCollectionBase& Collection) -{ - Super::Initialize(Collection); + while (!PendingRequests.IsEmpty()) + { + FPlatformProcess::Sleep(0.1f); - UTolgeeLocalizationSubsystem* LocalizationSubsystem = GEngine->GetEngineSubsystem(); - LocalizationSubsystem->ManualFetch(); + for (auto RequestIt = PendingRequests.CreateIterator(); RequestIt; ++RequestIt) + { + const FHttpRequestPtr& Request = *RequestIt; + if (EHttpRequestStatus::IsFinished(Request->GetStatus())) + { + const FHttpResponsePtr Response = Request->GetResponse(); + const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); + TSharedPtr JsonObject; + + if (FJsonSerializer::Deserialize(JsonReader, JsonObject)) + { + const TArray> LanguageStats = JsonObject->GetArrayField(TEXT("languageStats")); + for (const TSharedPtr& Language : LanguageStats) + { + const double LanguageUpdateTime = Language->AsObject()->GetNumberField(TEXT("translationsUpdatedAt")); + const int64 UnixTimestampSeconds = LanguageUpdateTime / 1000; + const FDateTime UpdateTime = FDateTime::FromUnixTimestamp(UnixTimestampSeconds); + + LastProjectUpdate = LastProjectUpdate < UpdateTime ? UpdateTime : LastProjectUpdate; + } + } + + RequestIt.RemoveCurrent(); + } + } + } - IMainFrameModule& MainFrameModule = IMainFrameModule::Get(); - if (MainFrameModule.IsWindowInitialized()) + if (LastProjectUpdate > LastFetchTime) { - const TSharedPtr RootWindow = FSlateApplication::Get().GetActiveTopLevelWindow(); - OnMainFrameReady(RootWindow, false); + ResetData(); + FetchAllProjects(); } else { - MainFrameModule.OnMainFrameCreationFinished().AddUObject(this, &UTolgeeEditorIntegrationSubsystem::OnMainFrameReady); - } - -#if ENGINE_MAJOR_VERSION > 4 - const bool bIsRunningCookCommandlet = IsRunningCookCommandlet(); -#else - const FString Commandline = FCommandLine::Get(); - const bool bIsRunningCookCommandlet = IsRunningCommandlet() && Commandline.Contains(TEXT("run=cook")); -#endif - - const UTolgeeSettings* Settings = GetDefault(); - if (bIsRunningCookCommandlet && !Settings->bLiveTranslationUpdates && Settings->bFetchTranslationsOnCook) - { - DownloadTranslationsJson(); + UE_LOG(LogTolgee, Display, TEXT("No new updates since last fetch at %s, last project update was at %s"), *LastFetchTime.ToString(), *LastProjectUpdate.ToString()); } } - -#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/TolgeeEditor/Private/TolgeeEditorIntegrationSubsystem.h b/Source/TolgeeEditor/Private/TolgeeEditorIntegrationSubsystem.h deleted file mode 100644 index 6ada2c5..0000000 --- a/Source/TolgeeEditor/Private/TolgeeEditorIntegrationSubsystem.h +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#pragma once - -#include -#include -#include - -#include "TolgeeEditorRequestData.h" - -#include "TolgeeEditorIntegrationSubsystem.generated.h" - -struct FLocalizedKey; -struct FTolgeeKeyData; - -class ULocalizationTarget; - -/** - * Subsystem responsible for updating the Tolgee backend based on the locally available data - * e.g.: upload local keys missing from remote, delete remote keys not present locally - */ -UCLASS() -class UTolgeeEditorIntegrationSubsystem : public UEditorSubsystem -{ - GENERATED_BODY() - -public: - /** - * Collects the local keys and fetches the remotes keys. After comparing the 2, it lets the users chose how to update the keys - */ - void Sync(); - /** - * Downloads the latest translations from the backend and updates the Translations.json file in the project - */ - void DownloadTranslationsJson(); - -private: - /** - * Ensures that the file is checked out if it's source controlled. - */ - bool EnsureFileCheckedOutSourceControl(FString FilePath); - /** - * Ensures that the file is added to source control if it was modified - * If it was not modified it will ensure that the file does not remain checked out - */ - bool EnsureAddedStateSourceControl(FString FilePath, bool bWasFileModified); - /** - * Sends a request to the backend to import new keys - */ - void UploadLocalKeys(TArray NewLocalKeys); - /** - * Callback executed after a request to import new keys is completed - */ - void OnLocalKeysUploaded(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); - /** - * Sends a request to the backend to delete unused keys - */ - void DeleteRemoteKeys(TArray UnusedRemoteKeys); - /** - * Callback executed after a request to delete unused remote keys is completed - */ - void OnRemoteKeysDeleted(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); - /** - * Sends multiple requests to the backend to update the outdated keys' tag based on the new source string - */ - void UpdateOutdatedKeys(TArray> OutdatedKeys); - /** - * Callback executed after a request to update an outdated key's tag is completed - */ - void OnOutdatedKeyUpdated(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful); - /** - * @brief Gathers all the Localization keys available in the GameTargetSet LocalizationTargets which are correctly configured - */ - TValueOrError, FText> GatherLocalKeys() const; - /** - * @brief Gathers all the localization keys available in the specified LocalizationTargets - */ - TArray GetKeysFromTargets(TArray LocalizationTargets) const; - /** - * @brief Determines which LocalizationTargets from the GameTargetSet are correctly configured - */ - TArray GatherValidLocalizationTargets() const; - /** - * @brief Callback executed when the editor main frame is ready to display the login pop-up - */ - void OnMainFrameReady(TSharedPtr InRootWindow, bool bIsRunningStartupDialog); - /** - * @brief Exports the locally available data to a file on disk to package it in the final build, returns true if the local file content was modified during the operation - */ - bool ExportLocalTranslations(); - - // Begin UEditorSubsystem interface - virtual void Initialize(FSubsystemCollectionBase& Collection) override; - // End UEditorSubsystem interface -}; diff --git a/Source/TolgeeEditor/Private/TolgeeEditorRequestData.cpp b/Source/TolgeeEditor/Private/TolgeeEditorRequestData.cpp deleted file mode 100644 index 418ac49..0000000 --- a/Source/TolgeeEditor/Private/TolgeeEditorRequestData.cpp +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#include "TolgeeEditorRequestData.h" diff --git a/Source/TolgeeEditor/Private/TolgeeEditorRequestData.h b/Source/TolgeeEditor/Private/TolgeeEditorRequestData.h deleted file mode 100644 index 53eb10b..0000000 --- a/Source/TolgeeEditor/Private/TolgeeEditorRequestData.h +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#pragma once - -#include - -#include "TolgeeEditorRequestData.generated.h" - -/** - * @brief Payload for the key purging request - */ -USTRUCT() -struct FKeysDeletePayload -{ - GENERATED_BODY() - - /** - * @brief Ids of the keys we will be deleting - */ - UPROPERTY() - TArray Ids; -}; - -/** - * @brief Representation of a Tolgee upload key - */ -USTRUCT() -struct FKeyItemPayload -{ - GENERATED_BODY() - - /** - * @brief Key's name in the namespace - */ - UPROPERTY() - FString Name; - /** - * @brief Namespace this key is part of - */ - UPROPERTY() - FString Namespace; - /** - * @brief Tags associated with this key - */ - UPROPERTY() - TArray Tags; -}; - -/** - * @brief Representation of a local localization key - */ -USTRUCT() -struct FLocalizationKey -{ - GENERATED_BODY() - - /** - * @brief Key's name in the namespace - */ - UPROPERTY() - FString Key; - /** - * @brief Namespace this key is part of - */ - UPROPERTY() - FString Namespace; - /** - * @brief Default text associcated with this key if no translation data is found - * @note also known as "intended usage" - */ - UPROPERTY() - FString DefaultText; -}; diff --git a/Source/TolgeeEditor/Private/TolgeeEditorSettings.cpp b/Source/TolgeeEditor/Private/TolgeeEditorSettings.cpp new file mode 100644 index 0000000..53c0a65 --- /dev/null +++ b/Source/TolgeeEditor/Private/TolgeeEditorSettings.cpp @@ -0,0 +1,8 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#include "TolgeeEditorSettings.h" + +FName UTolgeeEditorSettings::GetCategoryName() const +{ + return TEXT("Plugins"); +} diff --git a/Source/TolgeeEditor/Private/TolgeeStyle.cpp b/Source/TolgeeEditor/Private/TolgeeStyle.cpp index 7e50638..77c0c4c 100644 --- a/Source/TolgeeEditor/Private/TolgeeStyle.cpp +++ b/Source/TolgeeEditor/Private/TolgeeStyle.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #include "TolgeeStyle.h" @@ -6,43 +6,49 @@ #include #include #include +#include -TSharedPtr FTolgeeStyle::StyleSet = nullptr; +FName FTolgeeStyle::StyleName("TolgeeStyle"); +TUniquePtr FTolgeeStyle::Inst(nullptr); -TSharedPtr FTolgeeStyle::Get() +const FName& FTolgeeStyle::GetStyleSetName() const { - return StyleSet; + return StyleName; } -FName FTolgeeStyle::GetStyleSetName() +const FTolgeeStyle& FTolgeeStyle::Get() { - static FName TolgeeStyleName(TEXT("TolgeeStyle")); - return TolgeeStyleName; + ensure(Inst.IsValid()); + return *Inst.Get(); } void FTolgeeStyle::Initialize() { - // Only register once - if (StyleSet.IsValid()) + if (!Inst.IsValid()) { - return; + Inst = TUniquePtr(new FTolgeeStyle); } +} - StyleSet = MakeShared(GetStyleSetName()); - FString Root = IPluginManager::Get().FindPlugin(TEXT("Tolgee"))->GetBaseDir() / TEXT("Resources"); +void FTolgeeStyle::Shutdown() +{ + if (Inst.IsValid()) + { + FSlateStyleRegistry::UnRegisterSlateStyle(*Inst.Get()); + Inst.Reset(); + } +} - StyleSet->Set("Tolgee.Settings", new FSlateImageBrush(FName(*(Root + "/Settings-Icon.png")), FVector2D(64, 64))); +FTolgeeStyle::FTolgeeStyle() : FSlateStyleSet(StyleName) +{ - FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get()); -}; + SetParentStyleName(FAppStyle::GetAppStyleSetName()); + FSlateStyleSet::SetContentRoot(IPluginManager::Get().FindPlugin(TEXT("Tolgee"))->GetBaseDir() / TEXT("Resources")); -void FTolgeeStyle::Shutdown() -{ - if (StyleSet.IsValid()) { - FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get()); - ensure(StyleSet.IsUnique()); - StyleSet.Reset(); + Set("Tolgee.Settings", new IMAGE_BRUSH("Settings-Icon", FVector2D(64, 64))); } + + FSlateStyleRegistry::RegisterSlateStyle(*this); } diff --git a/Source/TolgeeEditor/Private/TolgeeStyle.h b/Source/TolgeeEditor/Private/TolgeeStyle.h deleted file mode 100644 index f67806e..0000000 --- a/Source/TolgeeEditor/Private/TolgeeStyle.h +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. - -#pragma once - -#include - -/** - * @brief Declares the Tolgee extension visual style. - */ -class FTolgeeStyle -{ -public: - /** - * @brief Initializes the SlateStyle - */ - static void Initialize(); - /** - * @brief Deinitializes the SlateStyle - */ - static void Shutdown(); - - /** - * @brief Singleton getter for the SlateStyle class - */ - static TSharedPtr Get(); - /** - * @brief Convince getter for name of the current SlateStyle - */ - static FName GetStyleSetName(); - -private: - /** - * @brief Singleton instance of the SlateStyle - */ - static TSharedPtr StyleSet; -}; diff --git a/Source/TolgeeEditor/Private/STolgeeTranslationTab.h b/Source/TolgeeEditor/Public/STolgeeTranslationTab.h similarity index 61% rename from Source/TolgeeEditor/Private/STolgeeTranslationTab.h rename to Source/TolgeeEditor/Public/STolgeeTranslationTab.h index a92eecc..01fc246 100644 --- a/Source/TolgeeEditor/Private/STolgeeTranslationTab.h +++ b/Source/TolgeeEditor/Public/STolgeeTranslationTab.h @@ -1,4 +1,4 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #pragma once @@ -12,18 +12,18 @@ class SWebBrowser; class STolgeeTranslationTab : public SDockTab { public: - SLATE_BEGIN_ARGS(STolgeeTranslationTab) {} + SLATE_BEGIN_ARGS(STolgeeTranslationTab) + { + } + SLATE_END_ARGS() + /** * @brief Constructs the translation dashboard widget */ void Construct(const FArguments& InArgs); private: - /** - * @brief Callback executed when the DockTab is activated - */ - void ActiveTab(); /** * @brief Callback executed when the DockTab is deactivated */ @@ -36,6 +36,18 @@ class STolgeeTranslationTab : public SDockTab * @brief Callback executed when the debug service wants to draw on screen */ void DebugDrawCallback(UCanvas* Canvas, APlayerController* PC); + /** + * Runs an asynchronous request to find the matching Url to the given TolgeeKeyId. + */ + void ShowWidgetForAsync(const FString& TolgeeKeyId); + /** + * Updates the browser widget to display the Tolgee web editor for the given key. + */ + void ShowWidgetFor(const FString& TolgeeKeyId); + /** + * Finds the project id for the given TolgeeKeyId. + */ + FString FindProjectIdFor(const FString& TolgeeKeyId) const; /** * @brief Handle for the registered debug callback */ @@ -44,4 +56,8 @@ class STolgeeTranslationTab : public SDockTab * @brief Browser widget used to display tolgee web editor */ TSharedPtr Browser; -}; + /** + * Flag used to prevent multiple requests from being sent at the same time. + */ + TAtomic bRequestInProgress = false; +}; \ No newline at end of file diff --git a/Source/TolgeeEditor/Private/TolgeeEditor.h b/Source/TolgeeEditor/Public/TolgeeEditor.h similarity index 81% rename from Source/TolgeeEditor/Private/TolgeeEditor.h rename to Source/TolgeeEditor/Public/TolgeeEditor.h index 5198911..5365554 100644 --- a/Source/TolgeeEditor/Private/TolgeeEditor.h +++ b/Source/TolgeeEditor/Public/TolgeeEditor.h @@ -1,4 +1,4 @@ -// Copyright (c) Tolgee 2022-2023. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. #pragma once @@ -9,17 +9,6 @@ */ class FTolgeeEditorModule : public IModuleInterface { -public: - /** - * Gets a reference to the Tolgee Editor module instance - */ - static FTolgeeEditorModule& Get(); - /** - * Spawns the Tolgee widget or draws attention to it if it's already spawned - */ - void ActivateWindowTab(); - -private: // Begin IModuleInterface interface virtual void StartupModule() override; virtual void ShutdownModule() override; diff --git a/Source/TolgeeEditor/Public/TolgeeEditorIntegrationSubsystem.h b/Source/TolgeeEditor/Public/TolgeeEditorIntegrationSubsystem.h new file mode 100644 index 0000000..4209ca1 --- /dev/null +++ b/Source/TolgeeEditor/Public/TolgeeEditorIntegrationSubsystem.h @@ -0,0 +1,88 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include "TolgeeLocalizationInjectorSubsystem.h" + +#include + +#include "TolgeeEditorIntegrationSubsystem.generated.h" + +/** + * Subsystem responsible for fetching localization data directly from the Tolgee dashboard (without exporting or CDN publishing). + */ +UCLASS() +class UTolgeeEditorIntegrationSubsystem : public UTolgeeLocalizationInjectorSubsystem +{ + GENERATED_BODY() + +public: + /** + * Performs an immediate fetch of the localization data from the Tolgee dashboard. + */ + void ManualFetch(); + +private: + // ~ Begin UTolgeeLocalizationInjectorSubsystem interface + virtual void OnGameInstanceStart(UGameInstance* GameInstance) override; + virtual void OnGameInstanceEnd(bool bIsSimulating) override; + virtual TMap> GetDataToInject() const override; + // ~ End UTolgeeLocalizationInjectorSubsystem interface + + /* + * Runs multiple requests to fetch all projects from the Tolgee dashboard. + */ + void FetchAllProjects(); + /** + * Fetches the localization data from the Tolgee dashboard. + */ + void FetchFromDashboard(const FString& ProjectId, const FString& RequestUrl); + /** + * Callback function for when the Tolgee dashboard data is retrived. + */ + void OnFetchedFromDashboard(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString ProjectId); + /** + * Clears the cached translations and resets the request counters. + */ + void ResetData(); + /** + * Saves a request content to a temporary zip on disk and reads the translations from it. + */ + bool ReadTranslationsFromZipContent(const FString& ProjectId, const TArray& ResponseContent); + /** + * Callback function executed at a regular interval to refresh the localization data. + */ + void OnRefreshTick(); + /** + * Runs an asynchronous request to check if any updates are available for the projects. + */ + void FetchIUpdatesAreAvailableAsync(); + /** + * If any of the projects were updated, fetches resets the data and fetches the latest translations. + */ + void FetchIfProjectsWereUpdated(); + /** + * List of cached translations for each culture. + */ + TMap> CachedTranslations; + /** + * Counts the number of requests sent. + */ + int32 NumRequestsSent = 0; + /** + * Counts the number of requests completed. + */ + int32 NumRequestsCompleted = 0; + /* + * Handle for the refresh tick delegate used to constantly refresh the localization data. + */ + FTimerHandle RefreshTick; + /** + * Last time any translations were fetched from the Tolgee dashboard. + */ + FDateTime LastFetchTime = {0}; + /** + * Flag used to prevent multiple requests from being sent at the same time. + */ + TAtomic bRequestInProgress = false; +}; \ No newline at end of file diff --git a/Source/TolgeeEditor/Public/TolgeeEditorSettings.h b/Source/TolgeeEditor/Public/TolgeeEditorSettings.h new file mode 100644 index 0000000..d9699db --- /dev/null +++ b/Source/TolgeeEditor/Public/TolgeeEditorSettings.h @@ -0,0 +1,71 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include + +#include "TolgeeEditorSettings.generated.h" + +/** + * Contains all the configurable properties for a single Localization Target. + */ +USTRUCT() +struct FTolgeePerTargetSettings +{ + GENERATED_BODY() + + /** + * Id of the project in Tolgee. + */ + UPROPERTY(EditAnywhere, Category = "Tolgee Localization") + FString ProjectId; +}; + + +/* + * @brief Settings for the Tolgee editor-only functionality. + */ +UCLASS(config = Tolgee, defaultconfig) +class TOLGEEEDITOR_API UTolgeeEditorSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + /** + * Api Key used for requests authentication. + * IMPORTANT: This will be saved in plain text in the config file. + * IMPORTANT: If you have multiple Tolgee projects connected, use a Personal Access Token instead of the API key. + */ + UPROPERTY(Config, EditAnywhere, Category = "Tolgee") + FString ApiKey = TEXT(""); + + /** + * Api Url used for requests. + * IMPORTANT: Change this if you are using a self-hosted Tolgee instance. + */ + UPROPERTY(Config, EditAnywhere, Category = "Tolgee") + FString ApiUrl = TEXT("https://app.tolgee.io"); + + /** + * Project IDs we want to fetch translations for during editor PIE sessions. + */ + UPROPERTY(Config, EditAnywhere, Category = "Tolgee|In-Context") + TArray ProjectIds; + + /** + * How often we should check the projects above for updates. + */ + UPROPERTY(Config, EditAnywhere, Category = "Tolgee|In-Context") + float RefreshInterval = 20.0f; + + /** + * Configurable settings for each localization target. + */ + UPROPERTY(Config, EditAnywhere, Category = "Tolgee|Provider") + TMap PerTargetSettings; + + // ~ Begin UDeveloperSettings Interface + virtual FName GetCategoryName() const override; + // ~ End UDeveloperSettings Interface +}; + diff --git a/Source/TolgeeEditor/Public/TolgeeStyle.h b/Source/TolgeeEditor/Public/TolgeeStyle.h new file mode 100644 index 0000000..ca5349e --- /dev/null +++ b/Source/TolgeeEditor/Public/TolgeeStyle.h @@ -0,0 +1,42 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include + +/** + * @brief Declares the Tolgee extension visual style. + */ +class FTolgeeStyle : public FSlateStyleSet +{ +public: + + /** + * @brief Access the singleton instance for this SlateStyle + */ + static const FTolgeeStyle& Get(); + /** + * @brief Creates and Registers the plugin SlateStyle + */ + static void Initialize(); + /** + * @brief Unregisters the plugin SlateStyle + */ + static void Shutdown(); + + // Begin FSlateStyleSet Interface + virtual const FName& GetStyleSetName() const override; + // End FSlateStyleSet Interface + +private: + FTolgeeStyle(); + + /** + * @brief Unique name for this SlateStyle + */ + static FName StyleName; + /** + * @brief Singleton instances of this SlateStyle. + */ + static TUniquePtr Inst; +}; diff --git a/Source/TolgeeEditor/TolgeeEditor.Build.cs b/Source/TolgeeEditor/TolgeeEditor.Build.cs index 54a4608..43aac98 100644 --- a/Source/TolgeeEditor/TolgeeEditor.Build.cs +++ b/Source/TolgeeEditor/TolgeeEditor.Build.cs @@ -1,48 +1,34 @@ -// Copyright (c) Tolgee. All Rights Reserved. +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. using UnrealBuildTool; public class TolgeeEditor : ModuleRules { - public TolgeeEditor(ReadOnlyTargetRules Target) : base(Target) - { - PublicDependencyModuleNames.AddRange( - new string[] - { - "Core", - } - ); + public TolgeeEditor(ReadOnlyTargetRules Target) : base(Target) + { + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "DeveloperSettings", + "EditorSubsystem", + "Engine", + "FileUtilities", + "HTTP", + "Json", + "JsonUtilities", + "Localization", + "LocalizationCommandletExecution", + "MainFrame", + "Projects", + "Slate", + "SlateCore", + "UnrealEd", + "WebBrowser", - PrivateDependencyModuleNames.AddRange( - new string[] - { - "CoreUObject", - "EditorSubsystem", - "Engine", - "HTTP", - "Json", - "JsonUtilities", - "Localization", - "LocalizationCommandletExecution", - "MainFrame", - "Projects", - "Slate", - "SlateCore", - "UnrealEd", - "WebBrowser", - - "Tolgee" - } - ); - - if (Target.Version.MajorVersion > 4) - { - PrivateDependencyModuleNames.AddRange( - new string[] - { - "DeveloperToolSettings", - } - ); - } - } -} + "Tolgee" + } + ); + } +} \ No newline at end of file diff --git a/Source/TolgeeProvider/Private/TolgeeLocalizationProvider.cpp b/Source/TolgeeProvider/Private/TolgeeLocalizationProvider.cpp new file mode 100644 index 0000000..2713cf6 --- /dev/null +++ b/Source/TolgeeProvider/Private/TolgeeLocalizationProvider.cpp @@ -0,0 +1,448 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#include "TolgeeLocalizationProvider.h" + +#include +#include +#include +#include +#include +#include + +#include "TolgeeProviderLocalizationServiceCommand.h" +#include "TolgeeProviderLocalizationServiceWorker.h" +#include "TolgeeEditorSettings.h" + +FString GetTempSubDirectory() +{ + return FPaths::ProjectSavedDir() / TEXT("Tolgee") / TEXT("Temp"); +} + +void FTolgeeLocalizationProvider::Init(bool bForceConnection) +{ +} + +void FTolgeeLocalizationProvider::Close() +{ +} + +const FName& FTolgeeLocalizationProvider::GetName() const +{ + static const FName ProviderName = "Tolgee"; + return ProviderName; +} + +const FText FTolgeeLocalizationProvider::GetDisplayName() const +{ + return NSLOCTEXT("Tolgee", "TolgeeProviderName", "Tolgee"); +} + +FText FTolgeeLocalizationProvider::GetStatusText() const +{ + checkf(false, TEXT("Not used in 5.5+")); + return FText::GetEmpty(); +} + +bool FTolgeeLocalizationProvider::IsEnabled() const +{ + return true; +} + +bool FTolgeeLocalizationProvider::IsAvailable() const +{ + return true; +} + +ELocalizationServiceOperationCommandResult::Type FTolgeeLocalizationProvider::GetState(const TArray& InTranslationIds, + TArray>& OutState, + ELocalizationServiceCacheUsage::Type InStateCacheUsage) +{ + return ELocalizationServiceOperationCommandResult::Succeeded; +} + +ELocalizationServiceOperationCommandResult::Type FTolgeeLocalizationProvider::Execute(const TSharedRef& InOperation, const TArray& InTranslationIds, ELocalizationServiceOperationConcurrency::Type InConcurrency, const FLocalizationServiceOperationComplete& InOperationCompleteDelegate) +{ + // Query to see if the we allow this operation + TSharedPtr Worker = CreateWorker(InOperation->GetName()); + if (!Worker.IsValid()) + { + // this operation is unsupported by this source control provider + FFormatNamedArguments Arguments; + Arguments.Add(TEXT("OperationName"), FText::FromName(InOperation->GetName())); + Arguments.Add(TEXT("ProviderName"), FText::FromName(GetName())); + FText Message(FText::Format(INVTEXT("Operation '{OperationName}' not supported by revision control provider '{ProviderName}'"), Arguments)); + FMessageLog("LocalizationService").Error(Message); + + (void)InOperationCompleteDelegate.ExecuteIfBound(InOperation, ELocalizationServiceOperationCommandResult::Failed); + return ELocalizationServiceOperationCommandResult::Failed; + } + + FTolgeeProviderLocalizationServiceCommand* Command = new FTolgeeProviderLocalizationServiceCommand(InOperation, Worker.ToSharedRef()); + Command->OperationCompleteDelegate = InOperationCompleteDelegate; + + // fire off operation + if (InConcurrency == ELocalizationServiceOperationConcurrency::Synchronous) + { + Command->bAutoDelete = false; + + return ExecuteSynchronousCommand(*Command); + } + + Command->bAutoDelete = true; + return IssueCommand(*Command); +} + +bool FTolgeeLocalizationProvider::CanCancelOperation(const TSharedRef& InOperation) const +{ + return false; +} + +void FTolgeeLocalizationProvider::CancelOperation(const TSharedRef& InOperation) +{ +} + +void FTolgeeLocalizationProvider::Tick() +{ + bool bStatesUpdated = false; + for (int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex) + { + FTolgeeProviderLocalizationServiceCommand& Command = *CommandQueue[CommandIndex]; + if (Command.bExecuteProcessed) + { + // Remove command from the queue + CommandQueue.RemoveAt(CommandIndex); + + // let command update the states of any files + bStatesUpdated |= Command.Worker->UpdateStates(); + + // dump any messages to output log + OutputCommandMessages(Command); + + Command.ReturnResults(); + + // commands that are left in the array during a tick need to be deleted + if (Command.bAutoDelete) + { + // Only delete commands that are not running 'synchronously' + delete &Command; + } + + // only do one command per tick loop, as we dont want concurrent modification + // of the command queue (which can happen in the completion delegate) + break; + } + } + + // NOTE: Currently. the ILocalizationServiceProvider doesn't have a StateChanged delegate like the ISourceControlProvider, but it might get one in the future. + //if (bStatesUpdated) + //{ + // OnSourceControlStateChanged.Broadcast(); + //} +} + +void FTolgeeLocalizationProvider::CustomizeSettingsDetails(IDetailCategoryBuilder& DetailCategoryBuilder) const +{ + UTolgeeEditorSettings* MutableSettings = GetMutableDefault(); + + FAddPropertyParams AddPropertyParams; + AddPropertyParams.HideRootObjectNode(true); + DetailCategoryBuilder.AddExternalObjectProperty({MutableSettings}, GET_MEMBER_NAME_CHECKED(UTolgeeEditorSettings, ApiUrl), EPropertyLocation::Common, AddPropertyParams); + DetailCategoryBuilder.AddExternalObjectProperty({MutableSettings}, GET_MEMBER_NAME_CHECKED(UTolgeeEditorSettings, ApiKey), EPropertyLocation::Common, AddPropertyParams); +} + +void FTolgeeLocalizationProvider::CustomizeTargetDetails(IDetailCategoryBuilder& DetailCategoryBuilder, TWeakObjectPtr LocalizationTarget) const +{ + checkf(false, TEXT("Not used in 5.5+")); +} + +void FTolgeeLocalizationProvider::CustomizeTargetToolbar(TSharedRef& MenuExtender, TWeakObjectPtr LocalizationTarget) const +{ + FTolgeeLocalizationProvider* ThisProvider = const_cast(this); + + MenuExtender->AddToolBarExtension( + "LocalizationService", + EExtensionHook::First, + nullptr, + FToolBarExtensionDelegate::CreateRaw(ThisProvider, &FTolgeeLocalizationProvider::AddTargetToolbarButtons, LocalizationTarget) + ); +} + +void FTolgeeLocalizationProvider::CustomizeTargetSetToolbar(TSharedRef& MenuExtender, TWeakObjectPtr LocalizationTargetSet) const +{ + FTolgeeLocalizationProvider* ThisProvider = const_cast(this); + + MenuExtender->AddToolBarExtension( + "LocalizationService", + EExtensionHook::First, + nullptr, + FToolBarExtensionDelegate::CreateRaw(ThisProvider, &FTolgeeLocalizationProvider::AddTargetSetToolbarButtons, LocalizationTargetSet) + ); +} + +void FTolgeeLocalizationProvider::AddTargetToolbarButtons(FToolBarBuilder& ToolbarBuilder, TWeakObjectPtr InLocalizationTarget) +{ + if (InLocalizationTarget->IsMemberOfEngineTargetSet()) + { + return; + } + + ToolbarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateRaw(this, &FTolgeeLocalizationProvider::ImportAllCulturesForTargetFromTolgee, InLocalizationTarget) + ), + NAME_None, + NSLOCTEXT("Tolgee", "TolgeeImportTarget", "Tolgee Pull"), + NSLOCTEXT("Tolgee", "TolgeeImportTargetTip", "Imports all cultures for this target from Tolgee"), + FSlateIcon(FAppStyle::GetAppStyleSetName(), "LocalizationDashboard.ImportTextAllTargetsAllCultures") + ); + + ToolbarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateRaw(this, &FTolgeeLocalizationProvider::ExportAllCulturesForTargetToTolgee, InLocalizationTarget) + ), + NAME_None, + NSLOCTEXT("Tolgee", "TolgeeExportTarget", "Tolgee Push"), + NSLOCTEXT("Tolgee", "TolgeeExportTargetTip", "Exports all cultures for this target to Tolgee"), + FSlateIcon(FAppStyle::GetAppStyleSetName(), "LocalizationDashboard.ExportTextAllTargetsAllCultures") + ); + + ToolbarBuilder.AddComboButton( + FUIAction(), + FOnGetContent::CreateRaw(this, &FTolgeeLocalizationProvider::CreateProjectSettingsWidget, InLocalizationTarget), + NSLOCTEXT("Tolgee", "TolgeeSettings", "Tolgee Settings"), + NSLOCTEXT("Tolgee", "TolgeeSettings", "Tolgee Settings"), + FSlateIcon(FAppStyle::GetAppStyleSetName(), "LocalizationDashboard.CompileTextAllTargetsAllCultures") + ); +} + +void FTolgeeLocalizationProvider::AddTargetSetToolbarButtons(FToolBarBuilder& ToolbarBuilder, TWeakObjectPtr InLocalizationTargetSet) +{ + if (InLocalizationTargetSet.IsValid() && InLocalizationTargetSet->TargetObjects.Num() > 0 && InLocalizationTargetSet->TargetObjects[0]->IsMemberOfEngineTargetSet()) + { + return; + } + + ToolbarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateRaw(this, &FTolgeeLocalizationProvider::ImportAllTargetsForSetFromTolgee, InLocalizationTargetSet) + ), + NAME_None, + NSLOCTEXT("Tolgee", "TolgeeImportSet", "Tolgee Pull"), + NSLOCTEXT("Tolgee", "TolgeeImportSetTip", "Imports all targets for this set from Tolgee"), + FSlateIcon(FAppStyle::GetAppStyleSetName(), "LocalizationDashboard.ImportTextAllTargetsAllCultures") + ); + + ToolbarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateRaw(this, &FTolgeeLocalizationProvider::ExportAllTargetsForSetToTolgee, InLocalizationTargetSet) + ), + NAME_None, + NSLOCTEXT("Tolgee", "TolgeeExportSet", "Tolgee Push"), + NSLOCTEXT("Tolgee", "TolgeeExportSetTip", "Exports all targets for this set to Tolgee"), + FSlateIcon(FAppStyle::GetAppStyleSetName(), "LocalizationDashboard.ExportTextAllTargetsAllCultures") + ); + +} + +void FTolgeeLocalizationProvider::ImportAllCulturesForTargetFromTolgee(TWeakObjectPtr LocalizationTarget) +{ + // Delete old files if they exists so we don't accidentally export old data + const FString AbsoluteFolderPath = FPaths::ConvertRelativePathToFull(GetTempSubDirectory() / LocalizationTarget->Settings.Name); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + PlatformFile.DeleteDirectoryRecursively(*AbsoluteFolderPath); + + TArray CultureStats = LocalizationTarget->Settings.SupportedCulturesStatistics; + FScopedSlowTask SlowTask(CultureStats.Num(), INVTEXT("Downloading Files from Localization Service...")); + for (FCultureStatistics CultureStat : CultureStats) + { + ILocalizationServiceProvider& Provider = ILocalizationServiceModule::Get().GetProvider(); + TSharedRef DownloadTargetFileOp = ILocalizationServiceOperation::Create(); + DownloadTargetFileOp->SetInTargetGuid(LocalizationTarget->Settings.Guid); + DownloadTargetFileOp->SetInLocale(CultureStat.CultureName); + + // NOTE: For some reason the base FDownloadLocalizationTargetFile prefers relative paths, so we will make it relative + FString Path = AbsoluteFolderPath / CultureStat.CultureName / LocalizationTarget->Settings.Name + ".po"; + FPaths::MakePathRelativeTo(Path, *FPaths::ProjectDir()); + DownloadTargetFileOp->SetInRelativeOutputFilePathAndName(Path); + + Provider.Execute(DownloadTargetFileOp); + SlowTask.EnterProgressFrame(1, FText::Format(INVTEXT("Downloading {0}"), FText::FromString(CultureStat.CultureName))); + } + + IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); + const TSharedPtr& MainFrameParentWindow = MainFrameModule.GetParentWindow(); + LocalizationCommandletTasks::ImportTextForTarget(MainFrameParentWindow.ToSharedRef(), LocalizationTarget.Get(), AbsoluteFolderPath); + UpdateTargetFromReports(LocalizationTarget); +} + +void FTolgeeLocalizationProvider::ExportAllCulturesForTargetToTolgee(TWeakObjectPtr LocalizationTarget) +{ + // Delete old files if they exists so we don't accidentally export old data + const FString AbsoluteFolderPath = FPaths::ConvertRelativePathToFull(GetTempSubDirectory() / LocalizationTarget->Settings.Name); + IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); + PlatformFile.DeleteDirectoryRecursively(*AbsoluteFolderPath); + + // Currently Unreal's format uses msgctx which is not supported by Tolgee, so we will use Crowdin format instead which doesn't use it. + // TODO: Revisit after https://github.com/tolgee/tolgee-platform/issues/3053 + LocalizationTarget->Settings.ExportSettings.POFormat = EPortableObjectFormat::Crowdin; + + IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); + const TSharedPtr& MainFrameParentWindow = MainFrameModule.GetParentWindow(); + LocalizationCommandletTasks::ExportTextForTarget(MainFrameParentWindow.ToSharedRef(), LocalizationTarget.Get(), AbsoluteFolderPath); + + TArray CultureStats = LocalizationTarget->Settings.SupportedCulturesStatistics; + FScopedSlowTask SlowTask(CultureStats.Num(), INVTEXT("Uploading Files to Localization Service...")); + for (FCultureStatistics CultureStat : CultureStats) + { + ILocalizationServiceProvider& Provider = ILocalizationServiceModule::Get().GetProvider(); + TSharedRef UploadFileOp = ILocalizationServiceOperation::Create(); + UploadFileOp->SetInTargetGuid(LocalizationTarget->Settings.Guid); + UploadFileOp->SetInLocale(CultureStat.CultureName); + + // NOTE: For some reason the base FUploadLocalizationTargetFile prefers relative paths, so we will make it relative + FString Path = AbsoluteFolderPath / CultureStat.CultureName / LocalizationTarget->Settings.Name + ".po"; + FPaths::MakePathRelativeTo(Path, *FPaths::ProjectDir()); + UploadFileOp->SetInRelativeInputFilePathAndName(Path); + + Provider.Execute(UploadFileOp); + SlowTask.EnterProgressFrame(1, FText::Format(INVTEXT("Uploading {0}"), FText::FromString(CultureStat.CultureName))); + } +} + +void FTolgeeLocalizationProvider::ImportAllTargetsForSetFromTolgee(TWeakObjectPtr LocalizationTargetSet) +{ + for (ULocalizationTarget* LocalizationTarget : LocalizationTargetSet->TargetObjects) + { + ImportAllCulturesForTargetFromTolgee(LocalizationTarget); + } +} + +void FTolgeeLocalizationProvider::ExportAllTargetsForSetToTolgee(TWeakObjectPtr LocalizationTargetSet) +{ + for (ULocalizationTarget* LocalizationTarget : LocalizationTargetSet->TargetObjects) + { + ExportAllCulturesForTargetToTolgee(LocalizationTarget); + } +} + +TSharedRef FTolgeeLocalizationProvider::CreateProjectSettingsWidget(TWeakObjectPtr InLocalizationTarget) +{ + const FGuid& TargetGuid = InLocalizationTarget->Settings.Guid; + + UTolgeeEditorSettings* MutableSettings = GetMutableDefault(); + FTolgeePerTargetSettings ProjectSettings = MutableSettings->PerTargetSettings.FindOrAdd(TargetGuid); + + FDetailsViewArgs DetailsViewArgs; + DetailsViewArgs.bAllowSearch = false; + DetailsViewArgs.bShowObjectLabel = false; + DetailsViewArgs.bShowScrollBar = false; + + FStructureDetailsViewArgs StructureViewArgs; + + TSharedPtr> Struct = MakeShared>(); + Struct->InitializeAs(ProjectSettings); + + FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); + TSharedPtr StructureDetailsView = PropertyEditorModule.CreateStructureDetailView(DetailsViewArgs, StructureViewArgs, Struct); + + StructureDetailsView->GetOnFinishedChangingPropertiesDelegate().AddLambda([MutableSettings, Struct, TargetGuid](const FPropertyChangedEvent& PropertyChangedEvent) + { + MutableSettings->PerTargetSettings[TargetGuid] = *Struct->Cast(); + MutableSettings->SaveConfig(); + }); + + return StructureDetailsView->GetWidget().ToSharedRef(); +} + +void FTolgeeLocalizationProvider::UpdateTargetFromReports(TWeakObjectPtr InLocalizationTarget) +{ + // NOTE: This function seems to be copy pasted in a lot of places FLocalizationTargetDetailCustomization/SLocalizationTargetEditorCultureRow(original), FLocalizationTargetSetDetailCustomizationm, UpdateTargetFromReports + ULocalizationTarget* LocalizationTarget = InLocalizationTarget.Get(); + LocalizationTarget->UpdateWordCountsFromCSV(); + LocalizationTarget->UpdateStatusFromConflictReport(); +} + +void FTolgeeLocalizationProvider::OutputCommandMessages(const FTolgeeProviderLocalizationServiceCommand& InCommand) const +{ + FMessageLog LocalizationServiceLog("LocalizationService"); + + for (int32 ErrorIndex = 0; ErrorIndex < InCommand.ErrorMessages.Num(); ++ErrorIndex) + { + LocalizationServiceLog.Error(FText::FromString(InCommand.ErrorMessages[ErrorIndex])); + } + + for (int32 InfoIndex = 0; InfoIndex < InCommand.InfoMessages.Num(); ++InfoIndex) + { + LocalizationServiceLog.Info(FText::FromString(InCommand.InfoMessages[InfoIndex])); + } +} + +TSharedPtr FTolgeeLocalizationProvider::CreateWorker(const FName& InOperationName) const +{ + if (const FGetTolgeeProviderLocalizationServiceWorker* Operation = WorkersMap.Find(InOperationName)) + { + return Operation->Execute(); + } + + return nullptr; +} + +ELocalizationServiceOperationCommandResult::Type FTolgeeLocalizationProvider::ExecuteSynchronousCommand(FTolgeeProviderLocalizationServiceCommand& InCommand) +{ + ELocalizationServiceOperationCommandResult::Type Result = ELocalizationServiceOperationCommandResult::Failed; + + // Display the progress dialog if a string was provided + { + // Issue the command asynchronously... + IssueCommand(InCommand); + + // ... then wait for its completion (thus making it synchronous) + while (!InCommand.bExecuteProcessed) + { + // Tick the command queue and update progress. + Tick(); + + // Sleep for a bit so we don't busy-wait so much. + FPlatformProcess::Sleep(0.01f); + } + + // always do one more Tick() to make sure the command queue is cleaned up. + Tick(); + + if (InCommand.bCommandSuccessful) + { + Result = ELocalizationServiceOperationCommandResult::Succeeded; + } + } + + // Delete the command now (asynchronous commands are deleted in the Tick() method) + check(!InCommand.bAutoDelete); + + // ensure commands that are not auto deleted do not end up in the command queue + if (CommandQueue.Contains(&InCommand)) + { + CommandQueue.Remove(&InCommand); + } + delete &InCommand; + + return Result; +} + +ELocalizationServiceOperationCommandResult::Type FTolgeeLocalizationProvider::IssueCommand(FTolgeeProviderLocalizationServiceCommand& InCommand) +{ + if (GThreadPool != nullptr) + { + // Queue this to our worker thread(s) for resolving + GThreadPool->AddQueuedWork(&InCommand); + CommandQueue.Add(&InCommand); + return ELocalizationServiceOperationCommandResult::Succeeded; + } + else + { + FText Message(INVTEXT("There are no threads available to process the localization service provider command.")); + FMessageLog("LocalizationService").Error(Message); + + InCommand.bCommandSuccessful = false; + return InCommand.ReturnResults(); + } +} diff --git a/Source/TolgeeProvider/Private/TolgeeProvider.cpp b/Source/TolgeeProvider/Private/TolgeeProvider.cpp new file mode 100644 index 0000000..3f4683a --- /dev/null +++ b/Source/TolgeeProvider/Private/TolgeeProvider.cpp @@ -0,0 +1,69 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#include "TolgeeProvider.h" + +#include +#include +#include + +#include "TolgeeLog.h" +#include "TolgeeProviderLocalizationServiceOperations.h" + +void FTolgeeProviderModule::StartupModule() +{ + DenyEditorSettings(); + + IModularFeatures::Get().RegisterModularFeature("LocalizationService", &TolgeeLocalizationProvider); + + TolgeeLocalizationProvider.RegisterWorker("UploadLocalizationTargetFile"); + TolgeeLocalizationProvider.RegisterWorker("DownloadLocalizationTargetFile"); + + // Currently, there is a bug in 5.5 where the saved settings are not getting applied + // because FLocalizationServiceSettings::LoadSettings reads from the `CoalescedSourceConfigs` config + ReApplySettings(); +} + +void FTolgeeProviderModule::ShutdownModule() +{ + IModularFeatures::Get().UnregisterModularFeature("LocalizationService", &TolgeeLocalizationProvider); +} + +void FTolgeeProviderModule::ReApplySettings() +{ + const FString& IniFile = LocalizationServiceHelpers::GetSettingsIni(); + if (FConfigFile* ConfigFile = GConfig->Find(IniFile)) + { + ConfigFile->Read(IniFile); + } + + ILocalizationServiceModule& LocalizationService = ILocalizationServiceModule::Get(); + FString SavedProvider; + GConfig->GetString(TEXT("LocalizationService.LocalizationServiceSettings"), TEXT("Provider"), SavedProvider, IniFile); + if (!SavedProvider.IsEmpty() && LocalizationService.GetProvider().GetName() != FName(SavedProvider)) + { + UE_LOG(LogTolgee, Display, TEXT("Applying saved Provider: %s"), *SavedProvider); + LocalizationService.SetProvider(FName(SavedProvider)); + } + + bool bSavedUseGlobalSettings = false; + const FString& GlobalIniFile = LocalizationServiceHelpers::GetGlobalSettingsIni(); + GConfig->GetBool(TEXT("LocalizationService.LocalizationServiceSettings"), TEXT("UseGlobalSettings"), bSavedUseGlobalSettings, GlobalIniFile); + if (LocalizationService.GetUseGlobalSettings() != bSavedUseGlobalSettings) + { + LocalizationService.SetUseGlobalSettings(bSavedUseGlobalSettings); + } +} + +void FTolgeeProviderModule::DenyEditorSettings() +{ + const FString TolgeeProviderSettingsSection = TEXT("/Script/TolgeeProvider.TolgeeProviderSettings"); + UProjectPackagingSettings* ProjectPackagingSettings = GetMutableDefault(); + if (!ProjectPackagingSettings->IniSectionDenylist.Contains(TolgeeProviderSettingsSection)) + { + UE_LOG(LogTolgee, Display, TEXT("Adding %s to ProjectPackagingSettings.IniSectionDenylist"), *TolgeeProviderSettingsSection); + ProjectPackagingSettings->IniSectionDenylist.Add(TolgeeProviderSettingsSection); + ProjectPackagingSettings->SaveConfig(); + } +} + +IMPLEMENT_MODULE(FTolgeeProviderModule, TolgeeProvider) \ No newline at end of file diff --git a/Source/TolgeeProvider/Private/TolgeeProviderLocalizationServiceCommand.cpp b/Source/TolgeeProvider/Private/TolgeeProviderLocalizationServiceCommand.cpp new file mode 100644 index 0000000..a58de6d --- /dev/null +++ b/Source/TolgeeProvider/Private/TolgeeProviderLocalizationServiceCommand.cpp @@ -0,0 +1,60 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#include "TolgeeProviderLocalizationServiceCommand.h" + +#include "TolgeeProviderLocalizationServiceWorker.h" + +FTolgeeProviderLocalizationServiceCommand::FTolgeeProviderLocalizationServiceCommand(const TSharedRef& InOperation, const TSharedRef& InWorker, const FLocalizationServiceOperationComplete& InOperationCompleteDelegate) + : Operation(InOperation) + , Worker(InWorker) + , OperationCompleteDelegate(InOperationCompleteDelegate) + , bExecuteProcessed(0) + , bCommandSuccessful(false) + , bAutoDelete(true) + , Concurrency(ELocalizationServiceOperationConcurrency::Synchronous) +{ + check(IsInGameThread()); +} + + +bool FTolgeeProviderLocalizationServiceCommand::DoWork() +{ + bCommandSuccessful = Worker->Execute(*this); + FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1); + + return bCommandSuccessful; +} + +void FTolgeeProviderLocalizationServiceCommand::Abandon() +{ + FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1); +} + +void FTolgeeProviderLocalizationServiceCommand::DoThreadedWork() +{ + Concurrency = ELocalizationServiceOperationConcurrency::Asynchronous; + DoWork(); +} + +ELocalizationServiceOperationCommandResult::Type FTolgeeProviderLocalizationServiceCommand::ReturnResults() +{ + // NOTE: Everything in this class is copied from the SourceControl implementation (Other Localization providers copied it too). + // Except this function where the Operation doesn't support adding messages, so we output them directly + + FMessageLog LocalizationServiceLog("LocalizationService"); + + for (FString& String : InfoMessages) + { + LocalizationServiceLog.Error(FText::FromString(String)); + } + for (FString& String : ErrorMessages) + { + LocalizationServiceLog.Info(FText::FromString(String)); + } + + // run the completion delegate if we have one bound + ELocalizationServiceOperationCommandResult::Type Result = bCommandSuccessful ? ELocalizationServiceOperationCommandResult::Succeeded : ELocalizationServiceOperationCommandResult::Failed; + (void)OperationCompleteDelegate.ExecuteIfBound(Operation, Result); + + return Result; +} \ No newline at end of file diff --git a/Source/TolgeeProvider/Private/TolgeeProviderLocalizationServiceOperations.cpp b/Source/TolgeeProvider/Private/TolgeeProviderLocalizationServiceOperations.cpp new file mode 100644 index 0000000..112b715 --- /dev/null +++ b/Source/TolgeeProvider/Private/TolgeeProviderLocalizationServiceOperations.cpp @@ -0,0 +1,194 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#include "TolgeeProviderLocalizationServiceOperations.h" + +#include +#include +#include + +#include "TolgeeProviderLocalizationServiceCommand.h" +#include "TolgeeEditorSettings.h" +#include "TolgeeLog.h" +#include "TolgeeProviderUtils.h" +#include "TolgeeUtils.h" + +FName FTolgeeProviderUploadFileWorker::GetName() const +{ + return "UploadFileWorker"; +} + +bool FTolgeeProviderUploadFileWorker::Execute(FTolgeeProviderLocalizationServiceCommand& InCommand) +{ + TSharedPtr UploadFileOp = StaticCastSharedRef(InCommand.Operation); + if (!UploadFileOp || !UploadFileOp.IsValid()) + { + UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Invalid operation")); + + InCommand.bCommandSuccessful = false; + return InCommand.bCommandSuccessful; + } + + const FString FilePathAndName = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir() / UploadFileOp->GetInRelativeInputFilePathAndName()); + FString FileContents; + if (!FFileHelper::LoadFileToString(FileContents, *FilePathAndName)) + { + UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Cannot load file %s"), *FilePathAndName); + + InCommand.bCommandSuccessful = false; + return InCommand.bCommandSuccessful; + } + + const FGuid TargetGuid = UploadFileOp->GetInTargetGuid(); + const FString Locale = UploadFileOp->GetInLocale(); + + //NOTE: This is currently not set by the Localization dashbard, when it becomes available send it as "removeOtherKeys" + const bool bRemoveMissingKeys = !UploadFileOp->GetPreserveAllText(); + + TSharedRef FileMapping = MakeShared(); + const FString FileName = FPaths::GetCleanFilename(FilePathAndName); + FileMapping->SetStringField(TEXT("fileName"), FileName); + + TSharedRef Params = MakeShared(); + Params->SetBoolField(TEXT("convertPlaceholdersToIcu"), false); + Params->SetBoolField(TEXT("createNewKeys"), true); + Params->SetArrayField(TEXT("fileMappings"), {MakeShared(FileMapping)}); + Params->SetStringField(TEXT("forceMode"), TEXT("KEEP")); + Params->SetBoolField(TEXT("overrideKeyDescriptions"), true); + Params->SetBoolField(TEXT("removeOtherKeys"), true); + Params->SetArrayField(TEXT("tagNewKeys"), {MakeShared(TEXT("UnrealSDK"))}); + + FString ParamsContents; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&ParamsContents); + FJsonSerializer::Serialize(Params, Writer); + + const UTolgeeEditorSettings* ProviderSettings = GetDefault(); + const FTolgeePerTargetSettings* ProjectSettings = ProviderSettings->PerTargetSettings.Find(TargetGuid); + const FString Url = FString::Printf(TEXT("%s/v2/projects/%s/single-step-import"), *ProviderSettings->ApiUrl, *ProjectSettings->ProjectId); + + FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); + HttpRequest->SetURL(Url); + HttpRequest->SetVerb(TEXT("POST")); + HttpRequest->SetHeader(TEXT("X-API-Key"), ProviderSettings->ApiKey); + TolgeeUtils::AddSdkHeaders(HttpRequest); + + const FString Boundary = "---------------------------" + FString::FromInt(FDateTime::Now().GetTicks()); + TolgeeProviderUtils::AddMultiRequestHeader(HttpRequest, Boundary); + TolgeeProviderUtils::AddMultiRequestPart(HttpRequest, Boundary, TEXT("name=\"params\""), ParamsContents); + TolgeeProviderUtils::AddMultiRequestPart(HttpRequest, Boundary, TEXT("name=\"files\"; filename=\"test.po\""), FileContents); + TolgeeProviderUtils::FinishMultiRequest(HttpRequest, Boundary); + + HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); + + HttpRequest->ProcessRequestUntilComplete(); + + FHttpResponsePtr Response = HttpRequest->GetResponse(); + if (!Response) + { + UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Failed to upload file %s"), *FilePathAndName); + + InCommand.bCommandSuccessful = false; + return InCommand.bCommandSuccessful; + } + if (!EHttpResponseCodes::IsOk(Response->GetResponseCode())) + { + UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Failed to upload file %s. Response code: %d"), *FilePathAndName, Response->GetResponseCode()); + UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Response: %s"), *Response->GetContentAsString()); + + InCommand.bCommandSuccessful = false; + return InCommand.bCommandSuccessful; + } + + UE_LOG(LogTolgee, Display, TEXT("FTolgeeProviderUploadFileWorker: Successfully uploaded file %s. Content: %s"), *FilePathAndName, *Response->GetContentAsString()); + + InCommand.bCommandSuccessful = true; + return InCommand.bCommandSuccessful; +} + +bool FTolgeeProviderUploadFileWorker::UpdateStates() const +{ + return true; +} + +FName FTolgeeProviderDownloadFileWorker::GetName() const +{ + return "DownloadFileWorker"; +} + +bool FTolgeeProviderDownloadFileWorker::Execute(FTolgeeProviderLocalizationServiceCommand& InCommand) +{ + TSharedPtr DownloadFileOp = StaticCastSharedRef(InCommand.Operation); + if (!DownloadFileOp || !DownloadFileOp.IsValid()) + { + UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderDownloadFileWorker: Invalid operation")); + + InCommand.bCommandSuccessful = false; + return InCommand.bCommandSuccessful; + } + + const FString FilePathAndName = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir() / DownloadFileOp->GetInRelativeOutputFilePathAndName()); + const FGuid TargetGuid = DownloadFileOp->GetInTargetGuid(); + const FString Locale = DownloadFileOp->GetInLocale(); + + const UTolgeeEditorSettings* ProviderSettings = GetDefault(); + const FTolgeePerTargetSettings* ProjectSettings = ProviderSettings->PerTargetSettings.Find(TargetGuid); + + const FString Url = FString::Printf(TEXT("%s/v2/projects/%s/export"), *ProviderSettings->ApiUrl, *ProjectSettings->ProjectId); + + FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); + HttpRequest->SetURL(Url); + HttpRequest->SetVerb(TEXT("POST")); + HttpRequest->SetHeader(TEXT("X-API-Key"), ProviderSettings->ApiKey); + HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + TolgeeUtils::AddSdkHeaders(HttpRequest); + + TSharedRef Body = MakeShared(); + Body->SetBoolField(TEXT("escapeHtml"), false); + Body->SetStringField(TEXT("format"), "PO"); + Body->SetBoolField(TEXT("supportArrays"), false); + Body->SetBoolField(TEXT("zip"), false); + Body->SetArrayField(TEXT("languages"), {MakeShared(Locale)}); + + FString RequestBody; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&RequestBody); + FJsonSerializer::Serialize(Body, Writer); + + HttpRequest->SetContentAsString(RequestBody); + HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); + + HttpRequest->ProcessRequestUntilComplete(); + + FHttpResponsePtr Response = HttpRequest->GetResponse(); + if (!Response) + { + UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Failed to download file %s"), *FilePathAndName); + + InCommand.bCommandSuccessful = false; + return InCommand.bCommandSuccessful; + } + if (!EHttpResponseCodes::IsOk(Response->GetResponseCode())) + { + UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Failed to download file %s. Response code: %d"), *FilePathAndName, Response->GetResponseCode()); + UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Response: %s"), *Response->GetContentAsString()); + + InCommand.bCommandSuccessful = false; + return InCommand.bCommandSuccessful; + } + + if (!FFileHelper::SaveStringToFile(Response->GetContentAsString(), *FilePathAndName, FFileHelper::EEncodingOptions::ForceUnicode)) + { + UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderDownloadFileWorker: Failed to write file %s"), *FilePathAndName); + + InCommand.bCommandSuccessful = false; + return InCommand.bCommandSuccessful; + } + + UE_LOG(LogTolgee, Display, TEXT("FTolgeeProviderUploadFileWorker: Successfully downloaded file %s. Content: %s"), *FilePathAndName, *Response->GetContentAsString()); + + InCommand.bCommandSuccessful = true; + return InCommand.bCommandSuccessful; +} + +bool FTolgeeProviderDownloadFileWorker::UpdateStates() const +{ + return true; +} \ No newline at end of file diff --git a/Source/TolgeeProvider/Private/TolgeeProviderUtils.cpp b/Source/TolgeeProvider/Private/TolgeeProviderUtils.cpp new file mode 100644 index 0000000..54fb8ec --- /dev/null +++ b/Source/TolgeeProvider/Private/TolgeeProviderUtils.cpp @@ -0,0 +1,43 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#include "TolgeeProviderUtils.h" + +void TolgeeProviderUtils::AddMultiRequestHeader(const FHttpRequestRef& HttpRequest, const FString& Boundary) +{ + HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("multipart/form-data; boundary =" + Boundary)); +} + +void TolgeeProviderUtils::AddMultiRequestPart(const FHttpRequestRef& HttpRequest, const FString& Boundary, const FString& ExtraHeaders, const FString& Value) +{ + const FString BoundaryBegin = FString(TEXT("--")) + Boundary + FString(TEXT("\r\n")); + + FString Data = GetRequestContent(HttpRequest); + Data += FString(TEXT("\r\n")) + + BoundaryBegin + + FString(TEXT("Content-Disposition: form-data;")) + + ExtraHeaders + + FString(TEXT("\r\n\r\n")) + + Value; + + HttpRequest->SetContentAsString(Data); +} + +void TolgeeProviderUtils::FinishMultiRequest(const FHttpRequestRef& HttpRequest, const FString& Boundary) +{ + const FString BoundaryEnd = FString(TEXT("\r\n--")) + Boundary + FString(TEXT("--\r\n")); + + FString Data = GetRequestContent(HttpRequest); + Data.Append(BoundaryEnd); + + HttpRequest->SetContentAsString(Data); +} + +FString TolgeeProviderUtils::GetRequestContent(const FHttpRequestRef& HttpRequest) +{ + TArray Content = HttpRequest->GetContent(); + + FString Result; + FFileHelper::BufferToString(Result, Content.GetData(), Content.Num()); + + return Result; +} \ No newline at end of file diff --git a/Source/TolgeeProvider/Public/TolgeeLocalizationProvider.h b/Source/TolgeeProvider/Public/TolgeeLocalizationProvider.h new file mode 100644 index 0000000..55d3cdb --- /dev/null +++ b/Source/TolgeeProvider/Public/TolgeeLocalizationProvider.h @@ -0,0 +1,104 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include "TolgeeProviderLocalizationServiceWorker.h" + +#include + +class FTolgeeProviderLocalizationServiceCommand; + +DECLARE_DELEGATE_RetVal(FTolgeeProviderLocalizationServiceWorkerRef, FGetTolgeeProviderLocalizationServiceWorker) + +class FTolgeeLocalizationProvider final : public ILocalizationServiceProvider +{ +public: + // ~Begin ILocalizationServiceProvider interface + virtual void Init(bool bForceConnection = true) override; + virtual void Close() override; + virtual const FName& GetName() const override; + virtual const FText GetDisplayName() const override; + virtual FText GetStatusText() const override; + virtual bool IsEnabled() const override; + virtual bool IsAvailable() const override; + virtual ELocalizationServiceOperationCommandResult::Type GetState(const TArray& InTranslationIds, TArray>& OutState, ELocalizationServiceCacheUsage::Type InStateCacheUsage) override; + virtual ELocalizationServiceOperationCommandResult::Type Execute(const TSharedRef& InOperation, const TArray& InTranslationIds, ELocalizationServiceOperationConcurrency::Type InConcurrency = ELocalizationServiceOperationConcurrency::Synchronous, const FLocalizationServiceOperationComplete& InOperationCompleteDelegate = FLocalizationServiceOperationComplete()) override; + virtual bool CanCancelOperation(const TSharedRef& InOperation) const override; + virtual void CancelOperation(const TSharedRef& InOperation) override; + virtual void Tick() override; + virtual void CustomizeSettingsDetails(IDetailCategoryBuilder& DetailCategoryBuilder) const override; + virtual void CustomizeTargetDetails(IDetailCategoryBuilder& DetailCategoryBuilder, TWeakObjectPtr LocalizationTarget) const override; + virtual void CustomizeTargetToolbar(TSharedRef& MenuExtender, TWeakObjectPtr LocalizationTarget) const override; + virtual void CustomizeTargetSetToolbar(TSharedRef& MenuExtender, TWeakObjectPtr LocalizationTargetSet) const override; + // ~End ILocalizationServiceProvider interface + + /** + * Add the buttons to the toolbar for this localization target + */ + void AddTargetToolbarButtons(FToolBarBuilder& ToolbarBuilder, TWeakObjectPtr InLocalizationTarget); + /** + * Add the buttons to the toolbar for this set of localization targets + */ + void AddTargetSetToolbarButtons(FToolBarBuilder& ToolbarBuilder, TWeakObjectPtr InLocalizationTargetSet); + /** + * Download and import all translations for all cultures for the specified target from Tolgee + */ + void ImportAllCulturesForTargetFromTolgee(TWeakObjectPtr LocalizationTarget); + /** + * Export and upload all cultures for a localization target to Tolgee + */ + void ExportAllCulturesForTargetToTolgee(TWeakObjectPtr LocalizationTarget); + /** + * Download and import all translations for all cultures for all targets for the specified target set from Tolgee + */ + void ImportAllTargetsForSetFromTolgee(TWeakObjectPtr LocalizationTargetSet); + /** + * Export and upload all cultures for all targets for a localization target set to Tolgee + */ + void ExportAllTargetsForSetToTolgee(TWeakObjectPtr LocalizationTargetSet); + /** + * Create a widget to configure the target's settings. + * NOTE: This spawns a widget to edit the matching sub-property of the provider settings + */ + TSharedRef CreateProjectSettingsWidget(TWeakObjectPtr InLocalizationTarget); + /** + * Refreshes the localization dashboard UI, specifically the word counter + */ + void UpdateTargetFromReports(TWeakObjectPtr InLocalizationTarget); + /** + * Sends messages from the command to the output log + */ + void OutputCommandMessages(const FTolgeeProviderLocalizationServiceCommand& InCommand) const; + /** + * Helper function to register a worker type with this provider + */ + template + void RegisterWorker(const FName& InName); + /** + * Create work instances for the specified operation + */ + TSharedPtr CreateWorker(const FName& InOperationName) const; + /** + * Execute the command synchronously + */ + ELocalizationServiceOperationCommandResult::Type ExecuteSynchronousCommand(FTolgeeProviderLocalizationServiceCommand& InCommand); + /** + * Execute the command asynchronously + */ + ELocalizationServiceOperationCommandResult::Type IssueCommand(FTolgeeProviderLocalizationServiceCommand& InCommand); + + TMap WorkersMap; + + TArray CommandQueue; +}; + +template +void FTolgeeLocalizationProvider::RegisterWorker(const FName& InName) +{ + FGetTolgeeProviderLocalizationServiceWorker Delegate = FGetTolgeeProviderLocalizationServiceWorker::CreateLambda([]() + { + return MakeShared(); + }); + + WorkersMap.Add(InName, Delegate); +} \ No newline at end of file diff --git a/Source/TolgeeProvider/Public/TolgeeProvider.h b/Source/TolgeeProvider/Public/TolgeeProvider.h new file mode 100644 index 0000000..12f95ad --- /dev/null +++ b/Source/TolgeeProvider/Public/TolgeeProvider.h @@ -0,0 +1,31 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include + +#include "TolgeeLocalizationProvider.h" + +/** + * Module responsible for introducing the Tolgee implementation of the LocalizationServiceProvider. + */ +class FTolgeeProviderModule : public IModuleInterface +{ + // ~Begin IModuleInterface interface + virtual void StartupModule() override; + virtual void ShutdownModule() override; + // ~End IModuleInterface interface + + /** + * Re-applys the settings from LocalizationService.LocalizationServiceSettings ini file. + */ + void ReApplySettings(); + /** + * Ensures editor settings are not packaged in the final game. + */ + void DenyEditorSettings(); + /** + * LocalizationServiceProvider instance for Tolgee. + */ + FTolgeeLocalizationProvider TolgeeLocalizationProvider; +}; \ No newline at end of file diff --git a/Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceCommand.h b/Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceCommand.h new file mode 100644 index 0000000..d16db30 --- /dev/null +++ b/Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceCommand.h @@ -0,0 +1,81 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include +#include + +class ITolgeeProviderLocalizationServiceWorker; + +class FTolgeeProviderLocalizationServiceCommand : public IQueuedWork +{ +public: + FTolgeeProviderLocalizationServiceCommand(const TSharedRef& InOperation, const TSharedRef& InWorker, const FLocalizationServiceOperationComplete& InOperationCompleteDelegate = FLocalizationServiceOperationComplete()); + + /** + * This function handles the core threaded operations. + * All tasks associated with this queued object should be executed within this method. + */ + bool DoWork(); + + /** + * Notifies queued work of abandonment for cleanup. Invoked only if abandoned before completion. + * The object must delete itself using its allocated heap. + */ + virtual void Abandon() override; + + /** + * Used to signal the object to clean up, but only after it has completed its work. + */ + virtual void DoThreadedWork() override; + + /** + * Save any results and call any registered callbacks. + */ + ELocalizationServiceOperationCommandResult::Type ReturnResults(); + + /** + * Operation we want to perform - contains outward-facing parameters & results + */ + TSharedRef Operation; + + /** + * The object that will actually do the work + */ + TSharedRef Worker; + + /** + * Delegate to notify when this operation completes + */ + FLocalizationServiceOperationComplete OperationCompleteDelegate; + + /** + * If true, this command has been processed by the Localization service thread + */ + volatile int32 bExecuteProcessed; + + /** + * If true, the Localization service command succeeded + */ + bool bCommandSuccessful; + + /** + * If true, this command will be automatically cleaned up in Tick() + */ + bool bAutoDelete; + + /** + * Whether we are running multi-treaded or not + */ + ELocalizationServiceOperationConcurrency::Type Concurrency; + + /** + * Info and/or warning message storage + */ + TArray InfoMessages; + + /** + * Potential error message storage + */ + TArray ErrorMessages; +}; \ No newline at end of file diff --git a/Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceOperations.h b/Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceOperations.h new file mode 100644 index 0000000..99c6337 --- /dev/null +++ b/Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceOperations.h @@ -0,0 +1,23 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include "TolgeeProviderLocalizationServiceWorker.h" + +class FTolgeeProviderUploadFileWorker : public ITolgeeProviderLocalizationServiceWorker +{ + // ~Begin ITolgeeProviderLocalizationServiceWorker + virtual FName GetName() const override; + virtual bool Execute(FTolgeeProviderLocalizationServiceCommand& InCommand) override; + virtual bool UpdateStates() const override; + // ~End ITolgeeProviderLocalizationServiceWorker +}; + +class FTolgeeProviderDownloadFileWorker : public ITolgeeProviderLocalizationServiceWorker +{ + // ~Begin ITolgeeProviderLocalizationServiceWorker + virtual FName GetName() const override; + virtual bool Execute(FTolgeeProviderLocalizationServiceCommand& InCommand) override; + virtual bool UpdateStates() const override; + // ~End ITolgeeProviderLocalizationServiceWorker +}; \ No newline at end of file diff --git a/Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceWorker.h b/Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceWorker.h new file mode 100644 index 0000000..a9f9eff --- /dev/null +++ b/Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceWorker.h @@ -0,0 +1,31 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +class FTolgeeProviderLocalizationServiceCommand; + +/** + * Base class for worker implementation that will be run to perform the commands. + */ +class ITolgeeProviderLocalizationServiceWorker +{ +public: + virtual ~ITolgeeProviderLocalizationServiceWorker() = default; + + /** + * Used to uniquely identify the worker type. + */ + virtual FName GetName() const = 0; + /** + * Used to perform any work that is necessary to complete the command. + * NOTE: Make sure you block the execution until the command is completed. + */ + virtual bool Execute(FTolgeeProviderLocalizationServiceCommand& InCommand) = 0; + /** + * Used to update the state of localization items after the command is completed. + * NOTE: This will always run on the main thread. + */ + virtual bool UpdateStates() const = 0; +}; + +using FTolgeeProviderLocalizationServiceWorkerRef = TSharedRef; \ No newline at end of file diff --git a/Source/TolgeeProvider/Public/TolgeeProviderUtils.h b/Source/TolgeeProvider/Public/TolgeeProviderUtils.h new file mode 100644 index 0000000..baef127 --- /dev/null +++ b/Source/TolgeeProvider/Public/TolgeeProviderUtils.h @@ -0,0 +1,25 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +#pragma once + +#include + +namespace TolgeeProviderUtils +{ + /** + * Adds the multi-part request header to the HTTP request. + */ + void AddMultiRequestHeader(const FHttpRequestRef& HttpRequest, const FString& Boundary); + /** + * Adds a multi-part request part to the HTTP with the given content based on the boundary and extra headers. + */ + void AddMultiRequestPart(const FHttpRequestRef& HttpRequest, const FString& Boundary, const FString& ExtraHeaders, const FString& Value); + /** + * Finishes the multi-part request by appending the boundary end to the HTTP request. + */ + void FinishMultiRequest(const FHttpRequestRef& HttpRequest, const FString& Boundary); + /** + * Gets the request content from the HTTP request as a string. + */ + FString GetRequestContent(const FHttpRequestRef& HttpRequest); +} \ No newline at end of file diff --git a/Source/TolgeeProvider/TolgeeProvider.Build.cs b/Source/TolgeeProvider/TolgeeProvider.Build.cs new file mode 100644 index 0000000..7813825 --- /dev/null +++ b/Source/TolgeeProvider/TolgeeProvider.Build.cs @@ -0,0 +1,30 @@ +// Copyright (c) Tolgee 2022-2025. All Rights Reserved. + +using UnrealBuildTool; + +public class TolgeeProvider : ModuleRules +{ + public TolgeeProvider(ReadOnlyTargetRules Target) : base(Target) + { + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "DeveloperToolSettings", + "DeveloperSettings", + "Engine", + "HTTP", + "Json", + "Localization", + "LocalizationCommandletExecution", + "LocalizationService", + "Slate", + "SlateCore", + + "Tolgee", + "TolgeeEditor", + } + ); + } +} \ No newline at end of file diff --git a/Tolgee.uplugin b/Tolgee.uplugin index 16f028a..c605d4d 100644 --- a/Tolgee.uplugin +++ b/Tolgee.uplugin @@ -1,35 +1,56 @@ { - "FileVersion": 3, - "VersionName": "0.1", - "EngineVersion": "5.3", - "FriendlyName": "Tolgee", - "Description": "Localization tool which makes the localization process simple. Easy to integrate to React, Angular and other applications.", - "Category" : "Localization", - "CreatedBy": "Tolgee", - "CreatedByURL": "https://tolgee.io/", - "DocsURL": "https://tolgee.io/integrations/unreal", - "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/2757e202f8f3408bbf66f65d26223398", - "SupportURL": "https://tolg.ee/slack", - "EnabledByDefault": false, - "CanContainContent": false, - "Modules": [ - { - "Name": "Tolgee", - "Type": "Runtime", - "LoadingPhase": "Default", - "WhitelistPlatforms": [ "Android", "IOS", "Linux", "Mac", "TVOS", "Win64" ] - }, - { - "Name": "TolgeeEditor", - "Type": "Editor", - "LoadingPhase": "Default", - "WhitelistPlatforms": [ "Win64", "Mac", "Linux" ] - } - ], - "Plugins": [ - { - "Name": "WebBrowserWidget", - "Enabled": true - } - ] -} + "FileVersion": 3, + "VersionName": "0.0", + "EngineVersion": "5.5", + "FriendlyName": "Tolgee", + "Description": "Localization tool which makes the localization process simple. Easy to integrate to React, Angular and other applications.", + "Category": "Localization", + "CreatedBy": "Tolgee", + "CreatedByURL": "https://tolgee.io/", + "DocsURL": "https://tolgee.io/integrations/unreal", + "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/2757e202f8f3408bbf66f65d26223398", + "SupportURL": "https://tolg.ee/slack", + "EnabledByDefault": false, + "CanContainContent": false, + "Modules": [ + { + "Name": "Tolgee", + "Type": "Runtime", + "LoadingPhase": "Default", + "WhitelistPlatforms": [ + "Android", + "IOS", + "Linux", + "Mac", + "TVOS", + "Win64" + ] + }, + { + "Name": "TolgeeEditor", + "Type": "Editor", + "LoadingPhase": "Default", + "WhitelistPlatforms": [ + "Linux", + "Mac", + "Win64" + ] + }, + { + "Name": "TolgeeProvider", + "Type": "Editor", + "LoadingPhase": "Default", + "WhitelistPlatforms": [ + "Win64", + "Mac", + "Linux" + ] + } + ], + "Plugins": [ + { + "Name": "WebBrowserWidget", + "Enabled": true + } + ] +} \ No newline at end of file