├── .clang-format ├── .github └── workflows │ ├── plugin-cd.yml │ └── plugin-ci.yml ├── .gitignore ├── Config ├── DefaultTolgee.ini └── FilterPlugin.ini ├── README.md ├── Resources ├── Icon128.png └── Settings-Icon.png ├── Source ├── Tolgee │ ├── Private │ │ ├── Tolgee.cpp │ │ ├── TolgeeCdnFetcherSubsystem.cpp │ │ ├── TolgeeLocalizationInjectorSubsystem.cpp │ │ ├── TolgeeLog.cpp │ │ ├── TolgeeRuntimeSettings.cpp │ │ ├── TolgeeTextSource.cpp │ │ └── TolgeeUtils.cpp │ ├── Public │ │ ├── Tolgee.h │ │ ├── TolgeeCdnFetcherSubsystem.h │ │ ├── TolgeeLocalizationInjectorSubsystem.h │ │ ├── TolgeeLog.h │ │ ├── TolgeeRuntimeSettings.h │ │ ├── TolgeeTextSource.h │ │ └── TolgeeUtils.h │ └── Tolgee.Build.cs ├── TolgeeEditor │ ├── Private │ │ ├── STolgeeTranslationTab.cpp │ │ ├── TolgeeEditor.cpp │ │ ├── TolgeeEditorIntegrationSubsystem.cpp │ │ ├── TolgeeEditorSettings.cpp │ │ └── TolgeeStyle.cpp │ ├── Public │ │ ├── STolgeeTranslationTab.h │ │ ├── TolgeeEditor.h │ │ ├── TolgeeEditorIntegrationSubsystem.h │ │ ├── TolgeeEditorSettings.h │ │ └── TolgeeStyle.h │ └── TolgeeEditor.Build.cs └── TolgeeProvider │ ├── Private │ ├── TolgeeLocalizationProvider.cpp │ ├── TolgeeProvider.cpp │ ├── TolgeeProviderLocalizationServiceCommand.cpp │ ├── TolgeeProviderLocalizationServiceOperations.cpp │ └── TolgeeProviderUtils.cpp │ ├── Public │ ├── TolgeeLocalizationProvider.h │ ├── TolgeeProvider.h │ ├── TolgeeProviderLocalizationServiceCommand.h │ ├── TolgeeProviderLocalizationServiceOperations.h │ ├── TolgeeProviderLocalizationServiceWorker.h │ └── TolgeeProviderUtils.h │ └── TolgeeProvider.Build.cs └── Tolgee.uplugin /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignConsecutiveAssignments: false 6 | AlignConsecutiveDeclarations: false 7 | AlignOperands: false 8 | AlignAfterOpenBracket: BlockIndent 9 | AlignTrailingComments: false 10 | AllowShortFunctionsOnASingleLine : InlineOnly 11 | AllowShortLambdasOnASingleLine: Empty 12 | AlwaysBreakTemplateDeclarations: Yes 13 | PointerAlignment: Left 14 | BraceWrapping: 15 | AfterCaseLabel: true 16 | AfterClass: true 17 | AfterControlStatement: true 18 | AfterEnum: true 19 | AfterFunction: true 20 | AfterNamespace: true 21 | AfterStruct: true 22 | AfterUnion: true 23 | AfterExternBlock: false 24 | BeforeCatch: true 25 | BeforeElse: true 26 | BeforeLambdaBody: true 27 | BeforeWhile: true 28 | SplitEmptyFunction: true 29 | SplitEmptyRecord: true 30 | SplitEmptyNamespace: true 31 | BreakBeforeBraces: Custom 32 | BinPackArguments: false 33 | BinPackParameters: false 34 | BreakConstructorInitializers: AfterColon 35 | ColumnLimit: 1000 36 | IncludeBlocks: Regroup 37 | IncludeCategories: 38 | - Regex: '.*\.generated\.h' 39 | Priority: 10 40 | SortPriority: 10 41 | - Regex: '^<.*\.h>' 42 | Priority: 1 43 | SortPriority: 1 44 | - Regex: '.*' 45 | Priority: 2 46 | SortPriority: 2 47 | IncludeIsMainRegex: '([-_](test|unittest))?$' 48 | IncludeIsMainSourceRegex: '' 49 | IndentCaseBlocks: true 50 | IndentWidth: 4 51 | MacroBlockBegin: '' 52 | MacroBlockEnd: '' 53 | MaxEmptyLinesToKeep: 2 54 | NamespaceIndentation: All 55 | SpacesInAngles: false 56 | TabWidth: 4 57 | UseTab: Always 58 | ... 59 | -------------------------------------------------------------------------------- /.github/workflows/plugin-cd.yml: -------------------------------------------------------------------------------- 1 | name: Plugin CD 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | plugin-cd: 10 | uses: outoftheboxplugins/BuildMachines/.github/workflows/release-to-github.yml@master 11 | with: 12 | plugin_name: Tolgee 13 | 14 | secrets: inherit -------------------------------------------------------------------------------- /.github/workflows/plugin-ci.yml: -------------------------------------------------------------------------------- 1 | name: Plugin CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | plugin-ci: 13 | uses: outoftheboxplugins/BuildMachines/.github/workflows/compile-plugin.yml@master 14 | secrets: 15 | DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} 16 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Binaries/ 2 | Intermediate/ -------------------------------------------------------------------------------- /Config/DefaultTolgee.ini: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /Config/FilterPlugin.ini: -------------------------------------------------------------------------------- 1 | [FilterPlugin] 2 | /.clang-format 3 | /.gitignore 4 | /README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tolgee Unreal Engine Plugin 2 | 3 | ![ci workflow](https://github.com/tolgee/tolgee-unreal/actions/workflows/plugin-ci.yml/badge.svg) 4 | [![twitter](https://img.shields.io/twitter/follow/Tolgee_i18n?style=social)](https://twitter.com/Tolgee_i18n) 5 | [![github stars](https://img.shields.io/github/stars/tolgee/tolgee-unreal?style=social)](https://github.com/tolgee/tolgee-unreal) 6 | [![slack](https://img.shields.io/badge/slack-Tolgee%20community-blue)](https://tolg.ee/slack) 7 | 8 | [Tolgee](https://tolgee.io) 9 | 10 | This repository contains the code for the Tolgee Unreal Engine Plugin. 11 | 12 | It provides ability to easily manage localization texts directly in your Unreal Engine project. 13 | 14 | To learn more visit [https://tolgee.io](https://tolgee.io) 15 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolgee/tolgee-unreal/335689068f69f2d5f7945a12ece343fb541476a5/Resources/Icon128.png -------------------------------------------------------------------------------- /Resources/Settings-Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolgee/tolgee-unreal/335689068f69f2d5f7945a12ece343fb541476a5/Resources/Settings-Icon.png -------------------------------------------------------------------------------- /Source/Tolgee/Private/Tolgee.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "Tolgee.h" 4 | 5 | IMPLEMENT_MODULE(FTolgeeModule, Tolgee) 6 | -------------------------------------------------------------------------------- /Source/Tolgee/Private/TolgeeCdnFetcherSubsystem.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeCdnFetcherSubsystem.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "TolgeeRuntimeSettings.h" 10 | #include "TolgeeLog.h" 11 | 12 | void UTolgeeCdnFetcherSubsystem::OnGameInstanceStart(UGameInstance* GameInstance) 13 | { 14 | const UTolgeeRuntimeSettings* Settings = GetDefault(); 15 | if (Settings->CdnAddresses.IsEmpty()) 16 | { 17 | UE_LOG(LogTolgee, Display, TEXT("No CDN addresses configured. Packaged builds will only use static data.")); 18 | return; 19 | } 20 | if (GIsEditor && !Settings->bUseCdnInEditor) 21 | { 22 | UE_LOG(LogTolgee, Display, TEXT("CDN was disabled for editor but it will be used in the final game.")); 23 | return; 24 | } 25 | 26 | FetchAllCdns(); 27 | } 28 | 29 | void UTolgeeCdnFetcherSubsystem::OnGameInstanceEnd(bool bIsSimulating) 30 | { 31 | ResetData(); 32 | 33 | LastModifiedDates.Empty(); 34 | } 35 | 36 | TMap> UTolgeeCdnFetcherSubsystem::GetDataToInject() const 37 | { 38 | return CachedTranslations; 39 | } 40 | 41 | void UTolgeeCdnFetcherSubsystem::FetchAllCdns() 42 | { 43 | const UTolgeeRuntimeSettings* Settings = GetDefault(); 44 | 45 | for (const FString& CdnAddress : Settings->CdnAddresses) 46 | { 47 | TArray Cultures = UKismetInternationalizationLibrary::GetLocalizedCultures(); 48 | for (const FString& Culture : Cultures) 49 | { 50 | const FString DownloadUrl = FString::Printf(TEXT("%s/%s.po"), *CdnAddress, *Culture); 51 | 52 | UE_LOG(LogTolgee, Display, TEXT("Fetching localization data for culture: %s from CDN: %s"), *Culture, *DownloadUrl); 53 | FetchFromCdn(Culture, DownloadUrl); 54 | } 55 | }; 56 | 57 | } 58 | 59 | void UTolgeeCdnFetcherSubsystem::FetchFromCdn(const FString& Culture, const FString& DownloadUrl) 60 | { 61 | const FString* LastModifiedDate = LastModifiedDates.Find(DownloadUrl); 62 | 63 | const FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); 64 | HttpRequest->SetVerb("GET"); 65 | HttpRequest->SetURL(DownloadUrl); 66 | HttpRequest->SetHeader(TEXT("accept"), TEXT("application/json")); 67 | 68 | if (LastModifiedDate) 69 | { 70 | HttpRequest->SetHeader(TEXT("If-Modified-Since"), *LastModifiedDate); 71 | } 72 | 73 | HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnFetchedFromCdn, Culture); 74 | HttpRequest->ProcessRequest(); 75 | 76 | NumRequestsSent++; 77 | } 78 | 79 | void UTolgeeCdnFetcherSubsystem::OnFetchedFromCdn(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString InCutlure) 80 | { 81 | if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) 82 | { 83 | UE_LOG(LogTolgee, Display, TEXT("Fetch successfully for %s to %s."), *InCutlure, *Request->GetURL()); 84 | 85 | TArray Translations = ExtractTranslationsFromPO(Response->GetContentAsString()); 86 | CachedTranslations.Emplace(InCutlure, Translations); 87 | 88 | const FString LastModified = Response->GetHeader(TEXT("Last-Modified")); 89 | if (!LastModified.IsEmpty()) 90 | { 91 | LastModifiedDates.Emplace(Request->GetURL(), LastModified); 92 | } 93 | } 94 | else if (Response.IsValid() && Response->GetResponseCode() == EHttpResponseCodes::NotModified) 95 | { 96 | UE_LOG(LogTolgee, Display, TEXT("No new data for %s to %s."), *InCutlure, *Request->GetURL()); 97 | } 98 | else 99 | { 100 | UE_LOG(LogTolgee, Error, TEXT("Request for %s to %s failed."), *InCutlure, *Request->GetURL()); 101 | } 102 | 103 | NumRequestsCompleted++; 104 | 105 | if (NumRequestsCompleted == NumRequestsSent) 106 | { 107 | UE_LOG(LogTolgee, Display, TEXT("All requests completed. Refreshing translation data.")); 108 | RefreshTranslationDataAsync(); 109 | } 110 | } 111 | 112 | void UTolgeeCdnFetcherSubsystem::ResetData() 113 | { 114 | NumRequestsSent = 0; 115 | NumRequestsCompleted = 0; 116 | 117 | CachedTranslations.Empty(); 118 | } -------------------------------------------------------------------------------- /Source/Tolgee/Private/TolgeeLocalizationInjectorSubsystem.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeLocalizationInjectorSubsystem.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #if WITH_LOCALIZATION_MODULE 11 | #include 12 | #include 13 | #endif 14 | 15 | #include "TolgeeLog.h" 16 | #include "TolgeeTextSource.h" 17 | 18 | void UTolgeeLocalizationInjectorSubsystem::OnGameInstanceStart(UGameInstance* GameInstance) 19 | { 20 | } 21 | 22 | void UTolgeeLocalizationInjectorSubsystem::OnGameInstanceEnd(bool bIsSimulating) 23 | { 24 | } 25 | 26 | void UTolgeeLocalizationInjectorSubsystem::GetLocalizedResources(const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource) const 27 | { 28 | TMap> DataToInject = GetDataToInject(); 29 | for (const TPair>& CachedTranslation : DataToInject) 30 | { 31 | if (!InPrioritizedCultures.Contains(CachedTranslation.Key)) 32 | { 33 | continue; 34 | } 35 | 36 | for (const FTolgeeTranslationData& TranslationData : CachedTranslation.Value) 37 | { 38 | const FTextKey InNamespace = TranslationData.ParsedNamespace; 39 | const FTextKey InKey = TranslationData.ParsedKey; 40 | const FString InLocalizedString = TranslationData.Translation; 41 | 42 | if (FTextLocalizationResource::FEntry* ExistingEntry = InOutLocalizedResource.Entries.Find(FTextId(InNamespace, InKey))) 43 | { 44 | //NOTE: -1 is a higher than usual priority, meaning this entry will override any existing one. See FTextLocalizationResource::ShouldReplaceEntry 45 | InOutLocalizedResource.AddEntry(InNamespace, InKey, ExistingEntry->SourceStringHash, InLocalizedString, -1); 46 | } 47 | else 48 | { 49 | #if UE_VERSION_NEWER_THAN(5, 5, 0) 50 | const FString InNamespaceString = InNamespace.ToString(); 51 | const FString InKeyString = InKey.ToString(); 52 | #else 53 | const FString InNamespaceString = InNamespace.GetChars(); 54 | const FString InKeyString = InKey.GetChars(); 55 | #endif 56 | UE_LOG(LogTolgee, Warning, TEXT("Failed to inject translation for %s:%s. Default entry not found."), *InNamespaceString, *InKeyString); 57 | } 58 | } 59 | } 60 | } 61 | 62 | TMap> UTolgeeLocalizationInjectorSubsystem::GetDataToInject() const 63 | { 64 | return {}; 65 | } 66 | 67 | void UTolgeeLocalizationInjectorSubsystem::Initialize(FSubsystemCollectionBase& Collection) 68 | { 69 | Super::Initialize(Collection); 70 | 71 | TextSource = MakeShared(); 72 | TextSource->GetLocalizedResources.BindUObject(this, &ThisClass::GetLocalizedResources); 73 | FTextLocalizationManager::Get().RegisterTextSource(TextSource.ToSharedRef()); 74 | 75 | FWorldDelegates::OnStartGameInstance.AddUObject(this, &ThisClass::OnGameInstanceStart); 76 | 77 | #if WITH_EDITOR 78 | FEditorDelegates::PrePIEEnded.AddUObject(this, &ThisClass::OnGameInstanceEnd); 79 | #endif 80 | } 81 | 82 | void UTolgeeLocalizationInjectorSubsystem::RefreshTranslationDataAsync() 83 | { 84 | UE_LOG(LogTolgee, Verbose, TEXT("RefreshTranslationDataAsync requested.")); 85 | 86 | AsyncTask( 87 | ENamedThreads::AnyHiPriThreadHiPriTask, 88 | [=]() 89 | { 90 | TRACE_CPUPROFILER_EVENT_SCOPE(UTolgeeLocalizationInjectorSubsystem::RefreshResources) 91 | 92 | UE_LOG(LogTolgee, Verbose, TEXT("RefreshTranslationDataAsync executing.")); 93 | 94 | FTextLocalizationManager::Get().RefreshResources(); 95 | } 96 | ); 97 | } 98 | 99 | TArray UTolgeeLocalizationInjectorSubsystem::ExtractTranslationsFromPO(const FString& PoContent) 100 | { 101 | TRACE_CPUPROFILER_EVENT_SCOPE(UTolgeeLocalizationInjectorSubsystem::ExtractTranslationsFromPO) 102 | 103 | TArray Result; 104 | 105 | #if WITH_LOCALIZATION_MODULE 106 | FPortableObjectFormatDOM PortableObject; 107 | PortableObject.FromString(PoContent); 108 | 109 | for (auto EntryPairIter = PortableObject.GetEntriesIterator(); EntryPairIter; ++EntryPairIter) 110 | { 111 | auto POEntry = EntryPairIter->Value; 112 | if (POEntry->MsgId.IsEmpty() || POEntry->MsgStr.Num() == 0 || POEntry->MsgStr[0].IsEmpty()) 113 | { 114 | // We ignore the header entry or entries with no translation. 115 | continue; 116 | } 117 | 118 | FTolgeeTranslationData TranslationData; 119 | 120 | constexpr ELocalizedTextCollapseMode InTextCollapseMode = ELocalizedTextCollapseMode::IdenticalTextIdAndSource; 121 | constexpr EPortableObjectFormat InPOFormat = EPortableObjectFormat::Crowdin; 122 | 123 | PortableObjectPipeline::ParseBasicPOFileEntry(*POEntry, TranslationData.ParsedNamespace, TranslationData.ParsedKey, TranslationData.SourceText, TranslationData.Translation, InTextCollapseMode, InPOFormat); 124 | 125 | Result.Add(TranslationData); 126 | } 127 | #else 128 | UE_LOG(LogTolgee, Error, TEXT("Localization module is not available. Cannot extract translations from PO content.")); 129 | #endif 130 | 131 | return Result; 132 | } -------------------------------------------------------------------------------- /Source/Tolgee/Private/TolgeeLog.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeLog.h" 4 | 5 | DEFINE_LOG_CATEGORY(LogTolgee); 6 | -------------------------------------------------------------------------------- /Source/Tolgee/Private/TolgeeRuntimeSettings.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeRuntimeSettings.h" 4 | 5 | FName UTolgeeRuntimeSettings::GetCategoryName() const 6 | { 7 | return TEXT("Plugins"); 8 | } -------------------------------------------------------------------------------- /Source/Tolgee/Private/TolgeeTextSource.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeTextSource.h" 4 | 5 | #include 6 | 7 | int32 FTolgeeTextSource::GetPriority() const 8 | { 9 | //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 10 | // Then, during the load we will inject the new entries the highest priority. 11 | return ELocalizedTextSourcePriority::Lowest; 12 | } 13 | 14 | bool FTolgeeTextSource::GetNativeCultureName(const ELocalizedTextSourceCategory InCategory, FString& OutNativeCultureName) 15 | { 16 | // TODO: Investigate if we should implement this 17 | return false; 18 | } 19 | 20 | void FTolgeeTextSource::GetLocalizedCultureNames(const ELocalizationLoadFlags InLoadFlags, TSet& OutLocalizedCultureNames) 21 | { 22 | // TODO: Investigate if we should implement this 23 | } 24 | 25 | void FTolgeeTextSource::LoadLocalizedResources(const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource) 26 | { 27 | GetLocalizedResources.Execute(InLoadFlags, InPrioritizedCultures, InOutNativeResource, InOutLocalizedResource); 28 | } -------------------------------------------------------------------------------- /Source/Tolgee/Private/TolgeeUtils.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeUtils.h" 4 | 5 | #include 6 | 7 | FString TolgeeUtils::AppendQueryParameters(const FString& BaseUrl, const TArray& Parameters) 8 | { 9 | FString FinalUrl = BaseUrl; 10 | TArray RemainingParameters = Parameters; 11 | 12 | if (RemainingParameters.Num() >= 1) 13 | { 14 | FinalUrl.Appendf(TEXT("?%s"), *RemainingParameters[0]); 15 | RemainingParameters.RemoveAt(0); 16 | } 17 | 18 | for (const FString& Parameter : RemainingParameters) 19 | { 20 | FinalUrl.Appendf(TEXT("&%s"), *Parameter); 21 | } 22 | 23 | return FinalUrl; 24 | } 25 | 26 | FString TolgeeUtils::GetSdkType() 27 | { 28 | return TEXT("Unreal"); 29 | } 30 | 31 | FString TolgeeUtils::GetSdkVersion() 32 | { 33 | const TSharedPtr TolgeePlugin = IPluginManager::Get().FindPlugin("Tolgee"); 34 | return TolgeePlugin->GetDescriptor().VersionName; 35 | } 36 | 37 | void TolgeeUtils::AddSdkHeaders(FHttpRequestRef& HttpRequest) 38 | { 39 | HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Type"), GetSdkType()); 40 | HttpRequest->SetHeader(TEXT("X-Tolgee-SDK-Version"), GetSdkVersion()); 41 | } -------------------------------------------------------------------------------- /Source/Tolgee/Public/Tolgee.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | /** 8 | * Base module for common Tolgee functionality and runtime features. 9 | */ 10 | class FTolgeeModule : public IModuleInterface 11 | { 12 | }; 13 | -------------------------------------------------------------------------------- /Source/Tolgee/Public/TolgeeCdnFetcherSubsystem.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "TolgeeLocalizationInjectorSubsystem.h" 6 | 7 | #include 8 | 9 | #include "TolgeeCdnFetcherSubsystem.generated.h" 10 | 11 | /** 12 | * Subsystem responsible for fetching localization data from a CDN and injecting it into the game. 13 | */ 14 | UCLASS() 15 | class UTolgeeCdnFetcherSubsystem : public UTolgeeLocalizationInjectorSubsystem 16 | { 17 | GENERATED_BODY() 18 | 19 | // ~ Begin UTolgeeLocalizationInjectorSubsystem interface 20 | virtual void OnGameInstanceStart(UGameInstance* GameInstance) override; 21 | virtual void OnGameInstanceEnd(bool bIsSimulating) override; 22 | virtual TMap> GetDataToInject() const override; 23 | // ~ End UTolgeeLocalizationInjectorSubsystem interface 24 | 25 | /** 26 | * Runs multiple requests to fetch all projects from the CDN. 27 | */ 28 | void FetchAllCdns(); 29 | /** 30 | * Fetches the localization data from the CDN. 31 | */ 32 | void FetchFromCdn(const FString& Culture, const FString& DownloadUrl); 33 | /** 34 | * Callback function for when the CDN request is completed. 35 | */ 36 | void OnFetchedFromCdn(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString InCutlure); 37 | /** 38 | * Clears the cached translations and resets the request counters. 39 | */ 40 | void ResetData(); 41 | /** 42 | * List of cached translations for each culture. 43 | */ 44 | TMap> CachedTranslations; 45 | /** 46 | * Counts the number of requests sent. 47 | */ 48 | int32 NumRequestsSent = 0; 49 | /** 50 | * Counts the number of requests completed. 51 | */ 52 | int32 NumRequestsCompleted = 0; 53 | /** 54 | * Map storing the last modified dates of the translations. 55 | */ 56 | TMap LastModifiedDates; 57 | }; -------------------------------------------------------------------------------- /Source/Tolgee/Public/TolgeeLocalizationInjectorSubsystem.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | #include "TolgeeLocalizationInjectorSubsystem.generated.h" 8 | 9 | class UGameInstance; 10 | class FTolgeeTextSource; 11 | 12 | /** 13 | * Holds the parsed data from the PO file 14 | */ 15 | struct FTolgeeTranslationData 16 | { 17 | FString ParsedNamespace; 18 | FString ParsedKey; 19 | FString SourceText; 20 | FString Translation; 21 | }; 22 | 23 | /** 24 | * Base class for all subsystems responsible for dynamically injecting data at runtime (e.g.: CDN, Dashboard, etc.) 25 | */ 26 | UCLASS(Abstract) 27 | class TOLGEE_API UTolgeeLocalizationInjectorSubsystem : public UEngineSubsystem 28 | { 29 | GENERATED_BODY() 30 | 31 | protected: 32 | /** 33 | * Callback executed when the game instance is created and started. 34 | */ 35 | virtual void OnGameInstanceStart(UGameInstance* GameInstance); 36 | /** 37 | * Callback executed when the game instance is destroyed. 38 | */ 39 | virtual void OnGameInstanceEnd(bool bIsSimulating); 40 | /** 41 | * Callback executed when the TolgeeTextSource needs to load the localized resources. 42 | */ 43 | virtual void GetLocalizedResources(const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource) const; 44 | /** 45 | * Simplified getter to allow subclasses to provide their own data to inject for GetLocalizedResources. 46 | */ 47 | virtual TMap> GetDataToInject() const; 48 | /** 49 | * Triggers an async refresh of the LocalizationManager resources. 50 | */ 51 | void RefreshTranslationDataAsync(); 52 | /** 53 | * Converts PO content to a list of translation data. 54 | */ 55 | TArray ExtractTranslationsFromPO(const FString& PoContent); 56 | 57 | // Begin UEngineSubsystem interface 58 | virtual void Initialize(FSubsystemCollectionBase& Collection) override; 59 | // End UEngineSubsystem interface 60 | 61 | private: 62 | /** 63 | * Custom Localization Text Source that allows handling of Localized Resources via delegate 64 | */ 65 | TSharedPtr TextSource; 66 | }; 67 | -------------------------------------------------------------------------------- /Source/Tolgee/Public/TolgeeLog.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | TOLGEE_API DECLARE_LOG_CATEGORY_EXTERN(LogTolgee, Log, All) -------------------------------------------------------------------------------- /Source/Tolgee/Public/TolgeeRuntimeSettings.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | #include "TolgeeRuntimeSettings.generated.h" 8 | 9 | /** 10 | * @brief Settings for the Tolgee runtime functionality. 11 | * NOTE: Settings here will be packaged in the final game 12 | */ 13 | UCLASS(config = Tolgee, defaultconfig) 14 | class TOLGEE_API UTolgeeRuntimeSettings : public UDeveloperSettings 15 | { 16 | GENERATED_BODY() 17 | 18 | public: 19 | /** 20 | * Root addresses we should use to fetch the localization data from the CDN. 21 | */ 22 | UPROPERTY(Config, EditAnywhere, Category = "Tolgee|CDN") 23 | TArray CdnAddresses; 24 | 25 | /** 26 | * Easy toggle to disable the CDN functionality in the editor. 27 | */ 28 | UPROPERTY(Config, EditAnywhere, Category = "Tolgee|CDN") 29 | bool bUseCdnInEditor = false; 30 | 31 | // ~ Begin UDeveloperSettings Interface 32 | virtual FName GetCategoryName() const override; 33 | // ~ End UDeveloperSettings Interface 34 | }; -------------------------------------------------------------------------------- /Source/Tolgee/Public/TolgeeTextSource.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | #include 8 | 9 | using FGetLocalizedResources = TDelegate InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource)>; 10 | 11 | /** 12 | * Translation source data for injecting data fetched from Tolgee backend into the localization system. 13 | */ 14 | class TOLGEE_API FTolgeeTextSource : public ILocalizedTextSource 15 | { 16 | public: 17 | /** 18 | * Callback executed when the text source needs to load the localized resources 19 | */ 20 | FGetLocalizedResources GetLocalizedResources; 21 | 22 | private: 23 | // Begin ILocalizedTextSource interface 24 | virtual int32 GetPriority() const override; 25 | virtual bool GetNativeCultureName(const ELocalizedTextSourceCategory InCategory, FString& OutNativeCultureName) override; 26 | virtual void GetLocalizedCultureNames(const ELocalizationLoadFlags InLoadFlags, TSet& OutLocalizedCultureNames) override; 27 | virtual void LoadLocalizedResources(const ELocalizationLoadFlags InLoadFlags, TArrayView InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource) override; 28 | // End ILocalizedTextSource interface 29 | }; 30 | -------------------------------------------------------------------------------- /Source/Tolgee/Public/TolgeeUtils.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | #include 8 | 9 | namespace TolgeeUtils 10 | { 11 | /** 12 | * @brief Constructs an endpoint by appending query parameters to a base url 13 | */ 14 | FString TOLGEE_API AppendQueryParameters(const FString& BaseUrl, const TArray& Parameters); 15 | /** 16 | * @brief Sdk type of the Tolgee integration 17 | */ 18 | FString TOLGEE_API GetSdkType(); 19 | /** 20 | * @brief Sdk version of the Tolgee integration 21 | */ 22 | FString TOLGEE_API GetSdkVersion(); 23 | /** 24 | * Adds the Tolgee SDK type and version to the headers of the request 25 | */ 26 | void TOLGEE_API AddSdkHeaders(FHttpRequestRef& HttpRequest); 27 | } // namespace TolgeeUtils 28 | -------------------------------------------------------------------------------- /Source/Tolgee/Tolgee.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class Tolgee : ModuleRules 6 | { 7 | public Tolgee(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PublicDependencyModuleNames.AddRange( 10 | new string[] 11 | { 12 | "Core", 13 | "CoreUObject", 14 | "DeveloperSettings", 15 | "Engine", 16 | "HTTP", 17 | "Json", 18 | "JsonUtilities", 19 | "Projects", 20 | } 21 | ); 22 | 23 | if (Target.bBuildEditor) 24 | { 25 | PublicDependencyModuleNames.Add("UnrealEd"); 26 | } 27 | 28 | bool bLocalizationModuleAvailable = !Target.bIsEngineInstalled || Target.Configuration != UnrealTargetConfiguration.Shipping; 29 | if (bLocalizationModuleAvailable) 30 | { 31 | PublicDefinitions.Add("WITH_LOCALIZATION_MODULE=1"); 32 | PublicDependencyModuleNames.Add("Localization"); 33 | } 34 | else 35 | { 36 | PublicDefinitions.Add("WITH_LOCALIZATION_MODULE=0"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Source/TolgeeEditor/Private/STolgeeTranslationTab.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "STolgeeTranslationTab.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "TolgeeEditorIntegrationSubsystem.h" 15 | #include "TolgeeEditorSettings.h" 16 | #include "TolgeeLog.h" 17 | #include "TolgeeUtils.h" 18 | 19 | namespace 20 | { 21 | FName STextBlockType(TEXT("STextBlock")); 22 | } 23 | 24 | void STolgeeTranslationTab::Construct(const FArguments& InArgs) 25 | { 26 | const UTolgeeEditorSettings* Settings = GetDefault(); 27 | const FString LoginUrl = FString::Printf(TEXT("%s/login"), *Settings->ApiUrl); 28 | 29 | DrawHandle = UDebugDrawService::Register(TEXT("Game"), FDebugDrawDelegate::CreateSP(this, &STolgeeTranslationTab::DebugDrawCallback)); 30 | 31 | // clang-format off 32 | SDockTab::Construct( SDockTab::FArguments() 33 | .TabRole(NomadTab) 34 | .OnTabClosed_Raw(this, &STolgeeTranslationTab::CloseTab) 35 | [ 36 | SAssignNew(Browser, SWebBrowser) 37 | .InitialURL(LoginUrl) 38 | .ShowControls(false) 39 | .ShowErrorMessage(true) 40 | ]); 41 | // clang-format on 42 | 43 | FGlobalTabmanager::Get()->OnActiveTabChanged_Subscribe(FOnActiveTabChanged::FDelegate::CreateSP(this, &STolgeeTranslationTab::OnActiveTabChanged)); 44 | } 45 | 46 | void STolgeeTranslationTab::CloseTab(TSharedRef DockTab) 47 | { 48 | UDebugDrawService::Unregister(DrawHandle); 49 | } 50 | 51 | void STolgeeTranslationTab::OnActiveTabChanged(TSharedPtr PreviouslyActive, TSharedPtr NewlyActivated) 52 | { 53 | if (PreviouslyActive == AsShared()) 54 | { 55 | GEngine->GetEngineSubsystem()->ManualFetch(); 56 | } 57 | } 58 | 59 | void STolgeeTranslationTab::DebugDrawCallback(UCanvas* Canvas, APlayerController* PC) 60 | { 61 | if (!GEngine->GameViewport) 62 | { 63 | return; 64 | } 65 | 66 | TSharedPtr GameViewportWidget = GEngine->GameViewport->GetGameViewportWidget(); 67 | if (!GameViewportWidget.IsValid()) 68 | { 69 | return; 70 | } 71 | 72 | FSlateApplication& Application = FSlateApplication::Get(); 73 | FWidgetPath WidgetPath = Application.LocateWindowUnderMouse(Application.GetCursorPos(), Application.GetInteractiveTopLevelWindows()); 74 | 75 | #if UE_VERSION_NEWER_THAN(5, 0, 0) 76 | const bool bValidHover = WidgetPath.Widgets.Num() > 0 && WidgetPath.ContainsWidget(GameViewportWidget.Get()); 77 | #else 78 | const bool bValidHover = WidgetPath.Widgets.Num() > 0 && GameViewportWidget.IsValid() && WidgetPath.ContainsWidget(GameViewportWidget.ToSharedRef()); 79 | #endif 80 | 81 | if (bValidHover) 82 | { 83 | TSharedPtr CurrentHoveredWidget = WidgetPath.GetLastWidget(); 84 | if (CurrentHoveredWidget->GetType() == STextBlockType) 85 | { 86 | TSharedPtr CurrentTextBlock = StaticCastSharedPtr(CurrentHoveredWidget); 87 | 88 | // Calculate the Start & End in local space based on widget & parent viewport 89 | const FGeometry& HoveredGeometry = CurrentHoveredWidget->GetCachedGeometry(); 90 | const FGeometry& ViewportGeometry = GameViewportWidget->GetCachedGeometry(); 91 | 92 | // TODO: make this a setting 93 | const FVector2D Padding = FVector2D{0.2f, 0.2f}; 94 | 95 | const FVector2D UpperLeft = {0, 0}; 96 | const FVector2D LowerRight = {1, 1}; 97 | 98 | FVector2D Start = HoveredGeometry.GetAbsolutePositionAtCoordinates(UpperLeft) - ViewportGeometry.GetAbsolutePositionAtCoordinates(UpperLeft) + Padding; 99 | FVector2D End = HoveredGeometry.GetAbsolutePositionAtCoordinates(LowerRight) - ViewportGeometry.GetAbsolutePositionAtCoordinates(UpperLeft) - Padding; 100 | 101 | FBox2D Box = FBox2D(Start, End); 102 | // TODO: make this a setting 103 | const FLinearColor DrawColor = FLinearColor(FColor::Red); 104 | DrawDebugCanvas2DBox(Canvas, Box, DrawColor); 105 | 106 | // Get information about the currently hovered text 107 | const FText CurrentText = CurrentTextBlock->GetText(); 108 | const TOptional Namespace = FTextInspector::GetNamespace(CurrentText); 109 | const TOptional Key = FTextInspector::GetKey(CurrentText); 110 | 111 | // Update the browser widget if we have valid data and the URL is different 112 | if (Namespace && Key) 113 | { 114 | const FString CleanNamespace = TextNamespaceUtil::StripPackageNamespace(Namespace.GetValue()); 115 | 116 | // 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. 117 | const FString TolgeeKeyId = FPlatformHttp::UrlEncode(FString::Printf(TEXT("%s,%s"), *CleanNamespace, *Key.GetValue())); 118 | 119 | ShowWidgetForAsync(TolgeeKeyId); 120 | } 121 | } 122 | } 123 | } 124 | 125 | void STolgeeTranslationTab::ShowWidgetForAsync(const FString& TolgeeKeyId) 126 | { 127 | AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, 128 | [this, TolgeeKeyId]() 129 | { 130 | ShowWidgetFor(TolgeeKeyId); 131 | }); 132 | } 133 | 134 | void STolgeeTranslationTab::ShowWidgetFor(const FString& TolgeeKeyId) 135 | { 136 | if (bRequestInProgress) 137 | { 138 | return; 139 | } 140 | 141 | TGuardValue ScopedRequestProgress(bRequestInProgress, true); 142 | 143 | const FString ProjectId = FindProjectIdFor(TolgeeKeyId); 144 | if (ProjectId.IsEmpty()) 145 | { 146 | UE_LOG(LogTolgee, Warning, TEXT("No project found for key '%s'"), *TolgeeKeyId); 147 | return; 148 | } 149 | 150 | const UTolgeeEditorSettings* Settings = GetDefault(); 151 | 152 | const FString NewUrl = FString::Printf(TEXT("%s/projects/%s/translations/single?key=%s"), *Settings->ApiUrl, *ProjectId, *TolgeeKeyId); 153 | const FString CurrentUrl = Browser->GetUrl(); 154 | 155 | if (NewUrl != CurrentUrl && Browser->IsLoaded()) 156 | { 157 | UE_LOG(LogTolgee, Log, TEXT("CurrentWidget displayed from %s"), *NewUrl); 158 | 159 | Browser->LoadURL(NewUrl); 160 | } 161 | } 162 | 163 | FString STolgeeTranslationTab::FindProjectIdFor(const FString& TolgeeKeyId) const 164 | { 165 | const UTolgeeEditorSettings* Settings = GetDefault(); 166 | 167 | TMap PendingRequests; 168 | for (const FString& ProjectId : Settings->ProjectIds) 169 | { 170 | const FString RequestUrl = FString::Printf(TEXT("%s/v2/projects/%s/translations?filterKeyName=%s"), *Settings->ApiUrl, *ProjectId, *TolgeeKeyId); 171 | 172 | FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); 173 | HttpRequest->SetVerb("GET"); 174 | HttpRequest->SetURL(RequestUrl); 175 | HttpRequest->SetHeader(TEXT("X-API-Key"), Settings->ApiKey); 176 | TolgeeUtils::AddSdkHeaders(HttpRequest); 177 | 178 | HttpRequest->ProcessRequest(); 179 | 180 | PendingRequests.Add(ProjectId, HttpRequest); 181 | } 182 | 183 | while (!PendingRequests.IsEmpty()) 184 | { 185 | FPlatformProcess::Sleep(0.1f); 186 | 187 | for (auto RequestIt = PendingRequests.CreateIterator(); RequestIt; ++RequestIt) 188 | { 189 | const TPair Pair = *RequestIt; 190 | const FHttpRequestPtr Request = Pair.Value; 191 | const FString& ProjectId = Pair.Key; 192 | 193 | if (EHttpRequestStatus::IsFinished(Request->GetStatus())) 194 | { 195 | const FHttpResponsePtr Response = Request->GetResponse(); 196 | const FString ResponseContent = Response.IsValid() ? Response->GetContentAsString() : FString(); 197 | 198 | const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(ResponseContent); 199 | TSharedPtr JsonObject; 200 | 201 | if (FJsonSerializer::Deserialize(JsonReader, JsonObject)) 202 | { 203 | const TSharedPtr Embedded = JsonObject->GetObjectField(TEXT("_embedded")); 204 | const TArray> Keys = Embedded->GetArrayField(TEXT("keys")); 205 | if (!Keys.IsEmpty()) 206 | { 207 | return ProjectId; 208 | } 209 | } 210 | 211 | RequestIt.RemoveCurrent(); 212 | } 213 | } 214 | } 215 | 216 | return {}; 217 | } -------------------------------------------------------------------------------- /Source/TolgeeEditor/Private/TolgeeEditor.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeEditor.h" 4 | 5 | #include 6 | #include 7 | 8 | #include "STolgeeTranslationTab.h" 9 | #include "TolgeeEditorSettings.h" 10 | #include "TolgeeStyle.h" 11 | 12 | #define LOCTEXT_NAMESPACE "Tolgee" 13 | 14 | namespace 15 | { 16 | const FName MenuTabName = FName("TolgeeDashboardMenuTab"); 17 | } 18 | 19 | void FTolgeeEditorModule::StartupModule() 20 | { 21 | RegisterStyle(); 22 | RegisterWindowExtension(); 23 | RegisterToolbarExtension(); 24 | } 25 | 26 | void FTolgeeEditorModule::ShutdownModule() 27 | { 28 | UnregisterStyle(); 29 | UnregisterWindowExtension(); 30 | UnregisterToolbarExtension(); 31 | } 32 | 33 | void FTolgeeEditorModule::RegisterStyle() 34 | { 35 | FTolgeeStyle::Initialize(); 36 | } 37 | 38 | void FTolgeeEditorModule::UnregisterStyle() 39 | { 40 | FTolgeeStyle::Shutdown(); 41 | } 42 | 43 | void FTolgeeEditorModule::RegisterWindowExtension() 44 | { 45 | FTabSpawnerEntry& DashboardTab = FGlobalTabmanager::Get()->RegisterNomadTabSpawner( 46 | MenuTabName, 47 | FOnSpawnTab::CreateLambda( 48 | [](const FSpawnTabArgs& Args) 49 | { 50 | return SNew(STolgeeTranslationTab); 51 | } 52 | ) 53 | ); 54 | 55 | // clang-format off 56 | DashboardTab.SetDisplayName(LOCTEXT("DashboardName", "Tolgee Dashboard")) 57 | .SetTooltipText(LOCTEXT("DashboardTooltip", "Get an overview of your project state in a separate tab.")) 58 | .SetIcon(FSlateIcon(FTolgeeStyle::Get().GetStyleSetName(), "Tolgee.Settings")) 59 | .SetMenuType(ETabSpawnerMenuType::Hidden); 60 | // clang-format on 61 | } 62 | 63 | void FTolgeeEditorModule::UnregisterWindowExtension() 64 | { 65 | FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(MenuTabName); 66 | } 67 | 68 | void FTolgeeEditorModule::RegisterToolbarExtension() 69 | { 70 | FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); 71 | const TSharedPtr ExtensionManager = LevelEditorModule.GetToolBarExtensibilityManager(); 72 | 73 | #if ENGINE_MAJOR_VERSION > 4 74 | const FName ToolbarHook = TEXT("ProjectSettings"); 75 | #else 76 | const FName ToolbarHook = TEXT("Settings"); 77 | #endif 78 | 79 | ToolbarExtender = MakeShareable(new FExtender); 80 | ToolbarExtender->AddToolBarExtension(ToolbarHook, EExtensionHook::First, nullptr, FToolBarExtensionDelegate::CreateRaw(this, &FTolgeeEditorModule::ExtendToolbar)); 81 | ExtensionManager->AddExtender(ToolbarExtender); 82 | } 83 | 84 | void FTolgeeEditorModule::UnregisterToolbarExtension() 85 | { 86 | if (ToolbarExtender.IsValid()) 87 | { 88 | FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); 89 | const TSharedPtr ExtensionManager = LevelEditorModule.GetToolBarExtensibilityManager(); 90 | 91 | ExtensionManager->RemoveExtender(ToolbarExtender); 92 | ToolbarExtender.Reset(); 93 | } 94 | } 95 | 96 | void FTolgeeEditorModule::ExtendToolbar(FToolBarBuilder& Builder) 97 | { 98 | Builder.AddComboButton( 99 | FUIAction(), 100 | FOnGetContent::CreateLambda( 101 | [=]() 102 | { 103 | FMenuBuilder MenuBuilder(true, NULL); 104 | 105 | MenuBuilder.AddMenuEntry( 106 | LOCTEXT("TranslationDashboard", "Translation Tab"), 107 | LOCTEXT("TranslationDashboardTip", "Open a translation widget inside Unreal which allows you translate text by hovering on top"), 108 | FSlateIcon(), 109 | FUIAction(FExecuteAction::CreateLambda( 110 | []() 111 | { 112 | FGlobalTabmanager::Get()->TryInvokeTab(MenuTabName); 113 | } 114 | )) 115 | ); 116 | 117 | MenuBuilder.AddMenuEntry( 118 | LOCTEXT("WebDashboard", "Web dashboard"), 119 | LOCTEXT("WebDashboardTip", "Launches the Tolgee dashboard in your browser"), 120 | FSlateIcon(), 121 | FUIAction(FExecuteAction::CreateLambda( 122 | []() 123 | { 124 | const UTolgeeEditorSettings* Settings = GetDefault(); 125 | FPlatformProcess::LaunchURL(*Settings->ApiUrl, nullptr, nullptr); 126 | } 127 | )) 128 | ); 129 | 130 | MenuBuilder.AddMenuEntry( 131 | LOCTEXT("OpenSettings", "Open Settings"), 132 | LOCTEXT("OpenSettingsTip", "Open the plugin settings section"), 133 | FSlateIcon(), 134 | FUIAction(FExecuteAction::CreateLambda( 135 | []() 136 | { 137 | //TODO: Enable this after settings get unified 138 | //UTolgeeSettings::OpenSettings(); 139 | } 140 | )) 141 | ); 142 | 143 | MenuBuilder.AddMenuEntry( 144 | LOCTEXT("OpenDocumentation", "Open Documentation"), 145 | LOCTEXT("OpenDocumentationTip", "Open a step by step guide on how to use our integration"), 146 | FSlateIcon(), 147 | FUIAction(FExecuteAction::CreateLambda( 148 | []() 149 | { 150 | const TSharedPtr TolgeePlugin = IPluginManager::Get().FindPlugin("Tolgee"); 151 | const FString DocsURL = TolgeePlugin->GetDescriptor().DocsURL; 152 | FPlatformProcess::LaunchURL(*DocsURL, nullptr, nullptr); 153 | } 154 | )) 155 | ); 156 | 157 | MenuBuilder.AddMenuEntry( 158 | LOCTEXT("GetSupport", "Get support"), 159 | LOCTEXT("GetSupportTip", "Join our slack and get support directly"), 160 | FSlateIcon(), 161 | FUIAction(FExecuteAction::CreateLambda( 162 | []() 163 | { 164 | const TSharedPtr CleanProjectPlugin = IPluginManager::Get().FindPlugin("Tolgee"); 165 | const FString DocsURL = CleanProjectPlugin->GetDescriptor().SupportURL; 166 | FPlatformProcess::LaunchURL(*DocsURL, nullptr, nullptr); 167 | } 168 | )) 169 | ); 170 | 171 | MenuBuilder.AddMenuEntry( 172 | LOCTEXT("ReportIssue", "Report issue"), 173 | LOCTEXT("ReportIssueTip", "Report an issue on our GitHub"), 174 | FSlateIcon(), 175 | FUIAction(FExecuteAction::CreateLambda( 176 | []() 177 | { 178 | FPlatformProcess::LaunchURL(TEXT("https://github.com/tolgee/tolgee-unreal/issues"), nullptr, nullptr); 179 | } 180 | )) 181 | ); 182 | 183 | return MenuBuilder.MakeWidget(); 184 | } 185 | ), 186 | LOCTEXT("TolgeeDashboardSettingsCombo", "Tolgee Settings"), 187 | LOCTEXT("TolgeeDashboardSettingsCombo_ToolTip", "Tolgee Dashboard settings"), 188 | FSlateIcon(FTolgeeStyle::Get().GetStyleSetName(), "Tolgee.Settings"), 189 | false 190 | ); 191 | } 192 | 193 | #undef LOCTEXT_NAMESPACE 194 | 195 | IMPLEMENT_MODULE(FTolgeeEditorModule, TolgeeEditor) 196 | -------------------------------------------------------------------------------- /Source/TolgeeEditor/Private/TolgeeEditorIntegrationSubsystem.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeEditorIntegrationSubsystem.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "TolgeeEditorSettings.h" 12 | #include "TolgeeLog.h" 13 | #include "TolgeeRuntimeSettings.h" 14 | #include "TolgeeUtils.h" 15 | 16 | void UTolgeeEditorIntegrationSubsystem::ManualFetch() 17 | { 18 | FetchIUpdatesAreAvailableAsync(); 19 | } 20 | 21 | void UTolgeeEditorIntegrationSubsystem::OnGameInstanceStart(UGameInstance* GameInstance) 22 | { 23 | const UTolgeeRuntimeSettings* RuntimeSettings = GetDefault(); 24 | if (RuntimeSettings->bUseCdnInEditor) 25 | { 26 | 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.")); 27 | return; 28 | } 29 | 30 | const UTolgeeEditorSettings* EditorSettings = GetDefault(); 31 | if (EditorSettings->ProjectIds.IsEmpty()) 32 | { 33 | UE_LOG(LogTolgee, Display, TEXT("CDN was disabled in editor, but no projects are configured. Static data will be used instead.")); 34 | return; 35 | } 36 | 37 | FTimerDelegate Delegate = FTimerDelegate::CreateUObject(this, &ThisClass::OnRefreshTick); 38 | GameInstance->GetWorld()->GetTimerManager().SetTimer(RefreshTick, Delegate, EditorSettings->RefreshInterval, true); 39 | 40 | FetchAllProjects(); 41 | } 42 | 43 | void UTolgeeEditorIntegrationSubsystem::OnGameInstanceEnd(bool bIsSimulating) 44 | { 45 | ResetData(); 46 | } 47 | 48 | TMap> UTolgeeEditorIntegrationSubsystem::GetDataToInject() const 49 | { 50 | return CachedTranslations; 51 | } 52 | 53 | void UTolgeeEditorIntegrationSubsystem::FetchAllProjects() 54 | { 55 | const UTolgeeEditorSettings* Settings = GetDefault(); 56 | 57 | for (const FString& ProjectId : Settings->ProjectIds) 58 | { 59 | const FString RequestUrl = FString::Printf(TEXT("%s/v2/projects/%s/export?format=PO"), *Settings->ApiUrl, *ProjectId); 60 | UE_LOG(LogTolgee, Display, TEXT("Fetching localization data for project %s from Tolgee dashboard: %s"), *ProjectId, *RequestUrl); 61 | FetchFromDashboard(ProjectId, RequestUrl); 62 | } 63 | 64 | LastFetchTime = FDateTime::UtcNow(); 65 | } 66 | 67 | void UTolgeeEditorIntegrationSubsystem::FetchFromDashboard(const FString& ProjectId, const FString& RequestUrl) 68 | { 69 | const UTolgeeEditorSettings* Settings = GetDefault(); 70 | 71 | FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); 72 | HttpRequest->SetURL(RequestUrl); 73 | HttpRequest->SetVerb("GET"); 74 | HttpRequest->SetHeader(TEXT("X-API-Key"), Settings->ApiKey); 75 | TolgeeUtils::AddSdkHeaders(HttpRequest); 76 | 77 | HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnFetchedFromDashboard, ProjectId); 78 | HttpRequest->ProcessRequest(); 79 | 80 | NumRequestsSent++; 81 | } 82 | 83 | void UTolgeeEditorIntegrationSubsystem::OnFetchedFromDashboard(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString ProjectId) 84 | { 85 | if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) 86 | { 87 | UE_LOG(LogTolgee, Display, TEXT("Fetch successfully for %s to %s."), *ProjectId, *Request->GetURL()); 88 | ReadTranslationsFromZipContent(ProjectId, Response->GetContent()); 89 | } 90 | else 91 | { 92 | UE_LOG(LogTolgee, Error, TEXT("Request for %s to %s failed."), *ProjectId, *Request->GetURL()); 93 | } 94 | 95 | NumRequestsCompleted++; 96 | 97 | if (NumRequestsCompleted == NumRequestsSent) 98 | { 99 | UE_LOG(LogTolgee, Display, TEXT("All requests completed. Refreshing translation data.")); 100 | RefreshTranslationDataAsync(); 101 | } 102 | } 103 | 104 | void UTolgeeEditorIntegrationSubsystem::ResetData() 105 | { 106 | NumRequestsSent = 0; 107 | NumRequestsCompleted = 0; 108 | 109 | CachedTranslations.Empty(); 110 | LastFetchTime = {0}; 111 | } 112 | 113 | bool UTolgeeEditorIntegrationSubsystem::ReadTranslationsFromZipContent(const FString& ProjectId, const TArray& ResponseContent) 114 | { 115 | static FCriticalSection ReadFromFileCriticalSection; 116 | FScopeLock Lock(&ReadFromFileCriticalSection); 117 | 118 | const FString TempPath = FPaths::ProjectSavedDir() / TEXT("Tolgee") / TEXT("In-Context") / ProjectId + TEXT(".zip"); 119 | const bool bSaveSuccess = FFileHelper::SaveArrayToFile(ResponseContent, *TempPath); 120 | if (!bSaveSuccess) 121 | { 122 | UE_LOG(LogTolgee, Error, TEXT("Failed to save zip for project %s as file to %s"), *ProjectId, *TempPath); 123 | return false; 124 | } 125 | 126 | IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); 127 | 128 | IFileHandle* ArchiveFileHandle = PlatformFile.OpenRead(*TempPath); 129 | FZipArchiveReader ZipReader = {ArchiveFileHandle}; 130 | if (!ZipReader.IsValid()) 131 | { 132 | UE_LOG(LogTolgee, Error, TEXT("Failed to open zip for project %s located at %s"), *ProjectId, *TempPath); 133 | return false; 134 | } 135 | 136 | const TArray FileNames = ZipReader.GetFileNames(); 137 | for (const FString& FileName : FileNames) 138 | { 139 | TArray FileBuffer; 140 | if (ZipReader.TryReadFile(FileName, FileBuffer)) 141 | { 142 | const FString InCulture = FPaths::GetBaseFilename(FileName); 143 | const FString FileContents = FString(FileBuffer.Num(), UTF8_TO_TCHAR(FileBuffer.GetData())); 144 | 145 | TArray Translations = ExtractTranslationsFromPO(FileContents); 146 | CachedTranslations.FindOrAdd(InCulture).Append(Translations); 147 | } 148 | else 149 | { 150 | UE_LOG(LogTolgee, Warning, TEXT("Failed to read file %s for project %s inside zip at %s"), *FileName, *ProjectId, *TempPath); 151 | } 152 | } 153 | 154 | return true; 155 | } 156 | 157 | void UTolgeeEditorIntegrationSubsystem::OnRefreshTick() 158 | { 159 | FetchIUpdatesAreAvailableAsync(); 160 | } 161 | 162 | void UTolgeeEditorIntegrationSubsystem::FetchIUpdatesAreAvailableAsync() 163 | { 164 | AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, 165 | [this]() 166 | { 167 | FetchIfProjectsWereUpdated(); 168 | }); 169 | } 170 | 171 | void UTolgeeEditorIntegrationSubsystem::FetchIfProjectsWereUpdated() 172 | { 173 | if (bRequestInProgress) 174 | { 175 | return; 176 | } 177 | 178 | TGuardValue ScopedRequestProgress(bRequestInProgress, true); 179 | 180 | FDateTime LastProjectUpdate = {0}; 181 | TArray PendingRequests; 182 | const UTolgeeEditorSettings* Settings = GetDefault(); 183 | 184 | for (const FString& ProjectId : Settings->ProjectIds) 185 | { 186 | const FString RequestUrl = FString::Printf(TEXT("%s/v2/projects/%s/stats"), *Settings->ApiUrl, *ProjectId); 187 | 188 | FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); 189 | HttpRequest->SetVerb("GET"); 190 | HttpRequest->SetURL(RequestUrl); 191 | HttpRequest->SetHeader(TEXT("X-API-Key"), Settings->ApiKey); 192 | TolgeeUtils::AddSdkHeaders(HttpRequest); 193 | 194 | HttpRequest->ProcessRequest(); 195 | 196 | PendingRequests.Add(HttpRequest); 197 | } 198 | 199 | 200 | while (!PendingRequests.IsEmpty()) 201 | { 202 | FPlatformProcess::Sleep(0.1f); 203 | 204 | for (auto RequestIt = PendingRequests.CreateIterator(); RequestIt; ++RequestIt) 205 | { 206 | const FHttpRequestPtr& Request = *RequestIt; 207 | if (EHttpRequestStatus::IsFinished(Request->GetStatus())) 208 | { 209 | const FHttpResponsePtr Response = Request->GetResponse(); 210 | const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(Response->GetContentAsString()); 211 | TSharedPtr JsonObject; 212 | 213 | if (FJsonSerializer::Deserialize(JsonReader, JsonObject)) 214 | { 215 | const TArray> LanguageStats = JsonObject->GetArrayField(TEXT("languageStats")); 216 | for (const TSharedPtr& Language : LanguageStats) 217 | { 218 | const double LanguageUpdateTime = Language->AsObject()->GetNumberField(TEXT("translationsUpdatedAt")); 219 | const int64 UnixTimestampSeconds = LanguageUpdateTime / 1000; 220 | const FDateTime UpdateTime = FDateTime::FromUnixTimestamp(UnixTimestampSeconds); 221 | 222 | LastProjectUpdate = LastProjectUpdate < UpdateTime ? UpdateTime : LastProjectUpdate; 223 | } 224 | } 225 | 226 | RequestIt.RemoveCurrent(); 227 | } 228 | } 229 | } 230 | 231 | if (LastProjectUpdate > LastFetchTime) 232 | { 233 | ResetData(); 234 | FetchAllProjects(); 235 | } 236 | else 237 | { 238 | UE_LOG(LogTolgee, Display, TEXT("No new updates since last fetch at %s, last project update was at %s"), *LastFetchTime.ToString(), *LastProjectUpdate.ToString()); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Source/TolgeeEditor/Private/TolgeeEditorSettings.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeEditorSettings.h" 4 | 5 | FName UTolgeeEditorSettings::GetCategoryName() const 6 | { 7 | return TEXT("Plugins"); 8 | } 9 | -------------------------------------------------------------------------------- /Source/TolgeeEditor/Private/TolgeeStyle.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeStyle.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | FName FTolgeeStyle::StyleName("TolgeeStyle"); 12 | TUniquePtr FTolgeeStyle::Inst(nullptr); 13 | 14 | const FName& FTolgeeStyle::GetStyleSetName() const 15 | { 16 | return StyleName; 17 | } 18 | 19 | const FTolgeeStyle& FTolgeeStyle::Get() 20 | { 21 | ensure(Inst.IsValid()); 22 | return *Inst.Get(); 23 | } 24 | 25 | void FTolgeeStyle::Initialize() 26 | { 27 | if (!Inst.IsValid()) 28 | { 29 | Inst = TUniquePtr(new FTolgeeStyle); 30 | } 31 | } 32 | 33 | void FTolgeeStyle::Shutdown() 34 | { 35 | if (Inst.IsValid()) 36 | { 37 | FSlateStyleRegistry::UnRegisterSlateStyle(*Inst.Get()); 38 | Inst.Reset(); 39 | } 40 | } 41 | 42 | FTolgeeStyle::FTolgeeStyle() : FSlateStyleSet(StyleName) 43 | { 44 | 45 | SetParentStyleName(FAppStyle::GetAppStyleSetName()); 46 | 47 | FSlateStyleSet::SetContentRoot(IPluginManager::Get().FindPlugin(TEXT("Tolgee"))->GetBaseDir() / TEXT("Resources")); 48 | 49 | { 50 | Set("Tolgee.Settings", new IMAGE_BRUSH("Settings-Icon", FVector2D(64, 64))); 51 | } 52 | 53 | FSlateStyleRegistry::RegisterSlateStyle(*this); 54 | } 55 | -------------------------------------------------------------------------------- /Source/TolgeeEditor/Public/STolgeeTranslationTab.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | class SWebBrowser; 8 | 9 | /** 10 | * Nomad Tab used to display the web translation widget inside Unreal when an Textblock is hovered 11 | */ 12 | class STolgeeTranslationTab : public SDockTab 13 | { 14 | public: 15 | SLATE_BEGIN_ARGS(STolgeeTranslationTab) 16 | { 17 | } 18 | 19 | SLATE_END_ARGS() 20 | 21 | /** 22 | * @brief Constructs the translation dashboard widget 23 | */ 24 | void Construct(const FArguments& InArgs); 25 | 26 | private: 27 | /** 28 | * @brief Callback executed when the DockTab is deactivated 29 | */ 30 | void CloseTab(TSharedRef DockTab); 31 | /** 32 | * @brief Callback executed when the active tab is changed 33 | */ 34 | void OnActiveTabChanged(TSharedPtr NewlyActivated, TSharedPtr PreviouslyActive); 35 | /** 36 | * @brief Callback executed when the debug service wants to draw on screen 37 | */ 38 | void DebugDrawCallback(UCanvas* Canvas, APlayerController* PC); 39 | /** 40 | * Runs an asynchronous request to find the matching Url to the given TolgeeKeyId. 41 | */ 42 | void ShowWidgetForAsync(const FString& TolgeeKeyId); 43 | /** 44 | * Updates the browser widget to display the Tolgee web editor for the given key. 45 | */ 46 | void ShowWidgetFor(const FString& TolgeeKeyId); 47 | /** 48 | * Finds the project id for the given TolgeeKeyId. 49 | */ 50 | FString FindProjectIdFor(const FString& TolgeeKeyId) const; 51 | /** 52 | * @brief Handle for the registered debug callback 53 | */ 54 | FDelegateHandle DrawHandle; 55 | /** 56 | * @brief Browser widget used to display tolgee web editor 57 | */ 58 | TSharedPtr Browser; 59 | /** 60 | * Flag used to prevent multiple requests from being sent at the same time. 61 | */ 62 | TAtomic bRequestInProgress = false; 63 | }; -------------------------------------------------------------------------------- /Source/TolgeeEditor/Public/TolgeeEditor.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | /** 8 | * @brief TolgeeEditor module is responsible for integrating translation features into the editor (tab to allow in-editor translation, gather & upload local keys) 9 | */ 10 | class FTolgeeEditorModule : public IModuleInterface 11 | { 12 | // Begin IModuleInterface interface 13 | virtual void StartupModule() override; 14 | virtual void ShutdownModule() override; 15 | // End IModuleInterface interface 16 | 17 | /** 18 | * @brief Registers the SlateStyle for custom editor styling 19 | */ 20 | void RegisterStyle(); 21 | /** 22 | * @brief Unregisters the SlateStyle for custom editor styling 23 | */ 24 | void UnregisterStyle(); 25 | /** 26 | * @brief Register the custom nomad tab spawn for the dashboard 27 | */ 28 | void RegisterWindowExtension(); 29 | /** 30 | * @brief Unregister the custom nomad tab spawn for the dashboard 31 | */ 32 | void UnregisterWindowExtension(); 33 | /** 34 | * @brief Registers all the extensions for the Toolbar menu 35 | */ 36 | void RegisterToolbarExtension(); 37 | /** 38 | * @brief Unregisters all the extensions for the Toolbar menu 39 | */ 40 | void UnregisterToolbarExtension(); 41 | /** 42 | * @brief Fills the toolbar with all the buttons accessible to the user 43 | */ 44 | void ExtendToolbar(FToolBarBuilder& Builder); 45 | /** 46 | * @brief The extender passed to the level editor to extend it's toolbar 47 | */ 48 | TSharedPtr ToolbarExtender; 49 | }; 50 | -------------------------------------------------------------------------------- /Source/TolgeeEditor/Public/TolgeeEditorIntegrationSubsystem.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "TolgeeLocalizationInjectorSubsystem.h" 6 | 7 | #include 8 | 9 | #include "TolgeeEditorIntegrationSubsystem.generated.h" 10 | 11 | /** 12 | * Subsystem responsible for fetching localization data directly from the Tolgee dashboard (without exporting or CDN publishing). 13 | */ 14 | UCLASS() 15 | class UTolgeeEditorIntegrationSubsystem : public UTolgeeLocalizationInjectorSubsystem 16 | { 17 | GENERATED_BODY() 18 | 19 | public: 20 | /** 21 | * Performs an immediate fetch of the localization data from the Tolgee dashboard. 22 | */ 23 | void ManualFetch(); 24 | 25 | private: 26 | // ~ Begin UTolgeeLocalizationInjectorSubsystem interface 27 | virtual void OnGameInstanceStart(UGameInstance* GameInstance) override; 28 | virtual void OnGameInstanceEnd(bool bIsSimulating) override; 29 | virtual TMap> GetDataToInject() const override; 30 | // ~ End UTolgeeLocalizationInjectorSubsystem interface 31 | 32 | /* 33 | * Runs multiple requests to fetch all projects from the Tolgee dashboard. 34 | */ 35 | void FetchAllProjects(); 36 | /** 37 | * Fetches the localization data from the Tolgee dashboard. 38 | */ 39 | void FetchFromDashboard(const FString& ProjectId, const FString& RequestUrl); 40 | /** 41 | * Callback function for when the Tolgee dashboard data is retrived. 42 | */ 43 | void OnFetchedFromDashboard(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString ProjectId); 44 | /** 45 | * Clears the cached translations and resets the request counters. 46 | */ 47 | void ResetData(); 48 | /** 49 | * Saves a request content to a temporary zip on disk and reads the translations from it. 50 | */ 51 | bool ReadTranslationsFromZipContent(const FString& ProjectId, const TArray& ResponseContent); 52 | /** 53 | * Callback function executed at a regular interval to refresh the localization data. 54 | */ 55 | void OnRefreshTick(); 56 | /** 57 | * Runs an asynchronous request to check if any updates are available for the projects. 58 | */ 59 | void FetchIUpdatesAreAvailableAsync(); 60 | /** 61 | * If any of the projects were updated, fetches resets the data and fetches the latest translations. 62 | */ 63 | void FetchIfProjectsWereUpdated(); 64 | /** 65 | * List of cached translations for each culture. 66 | */ 67 | TMap> CachedTranslations; 68 | /** 69 | * Counts the number of requests sent. 70 | */ 71 | int32 NumRequestsSent = 0; 72 | /** 73 | * Counts the number of requests completed. 74 | */ 75 | int32 NumRequestsCompleted = 0; 76 | /* 77 | * Handle for the refresh tick delegate used to constantly refresh the localization data. 78 | */ 79 | FTimerHandle RefreshTick; 80 | /** 81 | * Last time any translations were fetched from the Tolgee dashboard. 82 | */ 83 | FDateTime LastFetchTime = {0}; 84 | /** 85 | * Flag used to prevent multiple requests from being sent at the same time. 86 | */ 87 | TAtomic bRequestInProgress = false; 88 | }; -------------------------------------------------------------------------------- /Source/TolgeeEditor/Public/TolgeeEditorSettings.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | #include "TolgeeEditorSettings.generated.h" 8 | 9 | /** 10 | * Contains all the configurable properties for a single Localization Target. 11 | */ 12 | USTRUCT() 13 | struct FTolgeePerTargetSettings 14 | { 15 | GENERATED_BODY() 16 | 17 | /** 18 | * Id of the project in Tolgee. 19 | */ 20 | UPROPERTY(EditAnywhere, Category = "Tolgee Localization") 21 | FString ProjectId; 22 | }; 23 | 24 | 25 | /* 26 | * @brief Settings for the Tolgee editor-only functionality. 27 | */ 28 | UCLASS(config = Tolgee, defaultconfig) 29 | class TOLGEEEDITOR_API UTolgeeEditorSettings : public UDeveloperSettings 30 | { 31 | GENERATED_BODY() 32 | 33 | public: 34 | /** 35 | * Api Key used for requests authentication. 36 | * IMPORTANT: This will be saved in plain text in the config file. 37 | * IMPORTANT: If you have multiple Tolgee projects connected, use a Personal Access Token instead of the API key. 38 | */ 39 | UPROPERTY(Config, EditAnywhere, Category = "Tolgee") 40 | FString ApiKey = TEXT(""); 41 | 42 | /** 43 | * Api Url used for requests. 44 | * IMPORTANT: Change this if you are using a self-hosted Tolgee instance. 45 | */ 46 | UPROPERTY(Config, EditAnywhere, Category = "Tolgee") 47 | FString ApiUrl = TEXT("https://app.tolgee.io"); 48 | 49 | /** 50 | * Project IDs we want to fetch translations for during editor PIE sessions. 51 | */ 52 | UPROPERTY(Config, EditAnywhere, Category = "Tolgee|In-Context") 53 | TArray ProjectIds; 54 | 55 | /** 56 | * How often we should check the projects above for updates. 57 | */ 58 | UPROPERTY(Config, EditAnywhere, Category = "Tolgee|In-Context") 59 | float RefreshInterval = 20.0f; 60 | 61 | /** 62 | * Configurable settings for each localization target. 63 | */ 64 | UPROPERTY(Config, EditAnywhere, Category = "Tolgee|Provider") 65 | TMap PerTargetSettings; 66 | 67 | // ~ Begin UDeveloperSettings Interface 68 | virtual FName GetCategoryName() const override; 69 | // ~ End UDeveloperSettings Interface 70 | }; 71 | 72 | -------------------------------------------------------------------------------- /Source/TolgeeEditor/Public/TolgeeStyle.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | /** 8 | * @brief Declares the Tolgee extension visual style. 9 | */ 10 | class FTolgeeStyle : public FSlateStyleSet 11 | { 12 | public: 13 | 14 | /** 15 | * @brief Access the singleton instance for this SlateStyle 16 | */ 17 | static const FTolgeeStyle& Get(); 18 | /** 19 | * @brief Creates and Registers the plugin SlateStyle 20 | */ 21 | static void Initialize(); 22 | /** 23 | * @brief Unregisters the plugin SlateStyle 24 | */ 25 | static void Shutdown(); 26 | 27 | // Begin FSlateStyleSet Interface 28 | virtual const FName& GetStyleSetName() const override; 29 | // End FSlateStyleSet Interface 30 | 31 | private: 32 | FTolgeeStyle(); 33 | 34 | /** 35 | * @brief Unique name for this SlateStyle 36 | */ 37 | static FName StyleName; 38 | /** 39 | * @brief Singleton instances of this SlateStyle. 40 | */ 41 | static TUniquePtr Inst; 42 | }; 43 | -------------------------------------------------------------------------------- /Source/TolgeeEditor/TolgeeEditor.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class TolgeeEditor : ModuleRules 6 | { 7 | public TolgeeEditor(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PublicDependencyModuleNames.AddRange( 10 | new string[] 11 | { 12 | "Core", 13 | "CoreUObject", 14 | "DeveloperSettings", 15 | "EditorSubsystem", 16 | "Engine", 17 | "FileUtilities", 18 | "HTTP", 19 | "Json", 20 | "JsonUtilities", 21 | "Localization", 22 | "LocalizationCommandletExecution", 23 | "MainFrame", 24 | "Projects", 25 | "Slate", 26 | "SlateCore", 27 | "UnrealEd", 28 | "WebBrowser", 29 | 30 | "Tolgee" 31 | } 32 | ); 33 | } 34 | } -------------------------------------------------------------------------------- /Source/TolgeeProvider/Private/TolgeeLocalizationProvider.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeLocalizationProvider.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "TolgeeProviderLocalizationServiceCommand.h" 13 | #include "TolgeeProviderLocalizationServiceWorker.h" 14 | #include "TolgeeEditorSettings.h" 15 | 16 | FString GetTempSubDirectory() 17 | { 18 | return FPaths::ProjectSavedDir() / TEXT("Tolgee") / TEXT("Temp"); 19 | } 20 | 21 | void FTolgeeLocalizationProvider::Init(bool bForceConnection) 22 | { 23 | } 24 | 25 | void FTolgeeLocalizationProvider::Close() 26 | { 27 | } 28 | 29 | const FName& FTolgeeLocalizationProvider::GetName() const 30 | { 31 | static const FName ProviderName = "Tolgee"; 32 | return ProviderName; 33 | } 34 | 35 | const FText FTolgeeLocalizationProvider::GetDisplayName() const 36 | { 37 | return NSLOCTEXT("Tolgee", "TolgeeProviderName", "Tolgee"); 38 | } 39 | 40 | FText FTolgeeLocalizationProvider::GetStatusText() const 41 | { 42 | checkf(false, TEXT("Not used in 5.5+")); 43 | return FText::GetEmpty(); 44 | } 45 | 46 | bool FTolgeeLocalizationProvider::IsEnabled() const 47 | { 48 | return true; 49 | } 50 | 51 | bool FTolgeeLocalizationProvider::IsAvailable() const 52 | { 53 | return true; 54 | } 55 | 56 | ELocalizationServiceOperationCommandResult::Type FTolgeeLocalizationProvider::GetState(const TArray& InTranslationIds, 57 | TArray>& OutState, 58 | ELocalizationServiceCacheUsage::Type InStateCacheUsage) 59 | { 60 | return ELocalizationServiceOperationCommandResult::Succeeded; 61 | } 62 | 63 | ELocalizationServiceOperationCommandResult::Type FTolgeeLocalizationProvider::Execute(const TSharedRef& InOperation, const TArray& InTranslationIds, ELocalizationServiceOperationConcurrency::Type InConcurrency, const FLocalizationServiceOperationComplete& InOperationCompleteDelegate) 64 | { 65 | // Query to see if the we allow this operation 66 | TSharedPtr Worker = CreateWorker(InOperation->GetName()); 67 | if (!Worker.IsValid()) 68 | { 69 | // this operation is unsupported by this source control provider 70 | FFormatNamedArguments Arguments; 71 | Arguments.Add(TEXT("OperationName"), FText::FromName(InOperation->GetName())); 72 | Arguments.Add(TEXT("ProviderName"), FText::FromName(GetName())); 73 | FText Message(FText::Format(INVTEXT("Operation '{OperationName}' not supported by revision control provider '{ProviderName}'"), Arguments)); 74 | FMessageLog("LocalizationService").Error(Message); 75 | 76 | (void)InOperationCompleteDelegate.ExecuteIfBound(InOperation, ELocalizationServiceOperationCommandResult::Failed); 77 | return ELocalizationServiceOperationCommandResult::Failed; 78 | } 79 | 80 | FTolgeeProviderLocalizationServiceCommand* Command = new FTolgeeProviderLocalizationServiceCommand(InOperation, Worker.ToSharedRef()); 81 | Command->OperationCompleteDelegate = InOperationCompleteDelegate; 82 | 83 | // fire off operation 84 | if (InConcurrency == ELocalizationServiceOperationConcurrency::Synchronous) 85 | { 86 | Command->bAutoDelete = false; 87 | 88 | return ExecuteSynchronousCommand(*Command); 89 | } 90 | 91 | Command->bAutoDelete = true; 92 | return IssueCommand(*Command); 93 | } 94 | 95 | bool FTolgeeLocalizationProvider::CanCancelOperation(const TSharedRef& InOperation) const 96 | { 97 | return false; 98 | } 99 | 100 | void FTolgeeLocalizationProvider::CancelOperation(const TSharedRef& InOperation) 101 | { 102 | } 103 | 104 | void FTolgeeLocalizationProvider::Tick() 105 | { 106 | bool bStatesUpdated = false; 107 | for (int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex) 108 | { 109 | FTolgeeProviderLocalizationServiceCommand& Command = *CommandQueue[CommandIndex]; 110 | if (Command.bExecuteProcessed) 111 | { 112 | // Remove command from the queue 113 | CommandQueue.RemoveAt(CommandIndex); 114 | 115 | // let command update the states of any files 116 | bStatesUpdated |= Command.Worker->UpdateStates(); 117 | 118 | // dump any messages to output log 119 | OutputCommandMessages(Command); 120 | 121 | Command.ReturnResults(); 122 | 123 | // commands that are left in the array during a tick need to be deleted 124 | if (Command.bAutoDelete) 125 | { 126 | // Only delete commands that are not running 'synchronously' 127 | delete &Command; 128 | } 129 | 130 | // only do one command per tick loop, as we dont want concurrent modification 131 | // of the command queue (which can happen in the completion delegate) 132 | break; 133 | } 134 | } 135 | 136 | // NOTE: Currently. the ILocalizationServiceProvider doesn't have a StateChanged delegate like the ISourceControlProvider, but it might get one in the future. 137 | //if (bStatesUpdated) 138 | //{ 139 | // OnSourceControlStateChanged.Broadcast(); 140 | //} 141 | } 142 | 143 | void FTolgeeLocalizationProvider::CustomizeSettingsDetails(IDetailCategoryBuilder& DetailCategoryBuilder) const 144 | { 145 | UTolgeeEditorSettings* MutableSettings = GetMutableDefault(); 146 | 147 | FAddPropertyParams AddPropertyParams; 148 | AddPropertyParams.HideRootObjectNode(true); 149 | DetailCategoryBuilder.AddExternalObjectProperty({MutableSettings}, GET_MEMBER_NAME_CHECKED(UTolgeeEditorSettings, ApiUrl), EPropertyLocation::Common, AddPropertyParams); 150 | DetailCategoryBuilder.AddExternalObjectProperty({MutableSettings}, GET_MEMBER_NAME_CHECKED(UTolgeeEditorSettings, ApiKey), EPropertyLocation::Common, AddPropertyParams); 151 | } 152 | 153 | void FTolgeeLocalizationProvider::CustomizeTargetDetails(IDetailCategoryBuilder& DetailCategoryBuilder, TWeakObjectPtr LocalizationTarget) const 154 | { 155 | checkf(false, TEXT("Not used in 5.5+")); 156 | } 157 | 158 | void FTolgeeLocalizationProvider::CustomizeTargetToolbar(TSharedRef& MenuExtender, TWeakObjectPtr LocalizationTarget) const 159 | { 160 | FTolgeeLocalizationProvider* ThisProvider = const_cast(this); 161 | 162 | MenuExtender->AddToolBarExtension( 163 | "LocalizationService", 164 | EExtensionHook::First, 165 | nullptr, 166 | FToolBarExtensionDelegate::CreateRaw(ThisProvider, &FTolgeeLocalizationProvider::AddTargetToolbarButtons, LocalizationTarget) 167 | ); 168 | } 169 | 170 | void FTolgeeLocalizationProvider::CustomizeTargetSetToolbar(TSharedRef& MenuExtender, TWeakObjectPtr LocalizationTargetSet) const 171 | { 172 | FTolgeeLocalizationProvider* ThisProvider = const_cast(this); 173 | 174 | MenuExtender->AddToolBarExtension( 175 | "LocalizationService", 176 | EExtensionHook::First, 177 | nullptr, 178 | FToolBarExtensionDelegate::CreateRaw(ThisProvider, &FTolgeeLocalizationProvider::AddTargetSetToolbarButtons, LocalizationTargetSet) 179 | ); 180 | } 181 | 182 | void FTolgeeLocalizationProvider::AddTargetToolbarButtons(FToolBarBuilder& ToolbarBuilder, TWeakObjectPtr InLocalizationTarget) 183 | { 184 | if (InLocalizationTarget->IsMemberOfEngineTargetSet()) 185 | { 186 | return; 187 | } 188 | 189 | ToolbarBuilder.AddToolBarButton( 190 | FUIAction( 191 | FExecuteAction::CreateRaw(this, &FTolgeeLocalizationProvider::ImportAllCulturesForTargetFromTolgee, InLocalizationTarget) 192 | ), 193 | NAME_None, 194 | NSLOCTEXT("Tolgee", "TolgeeImportTarget", "Tolgee Pull"), 195 | NSLOCTEXT("Tolgee", "TolgeeImportTargetTip", "Imports all cultures for this target from Tolgee"), 196 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "LocalizationDashboard.ImportTextAllTargetsAllCultures") 197 | ); 198 | 199 | ToolbarBuilder.AddToolBarButton( 200 | FUIAction( 201 | FExecuteAction::CreateRaw(this, &FTolgeeLocalizationProvider::ExportAllCulturesForTargetToTolgee, InLocalizationTarget) 202 | ), 203 | NAME_None, 204 | NSLOCTEXT("Tolgee", "TolgeeExportTarget", "Tolgee Push"), 205 | NSLOCTEXT("Tolgee", "TolgeeExportTargetTip", "Exports all cultures for this target to Tolgee"), 206 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "LocalizationDashboard.ExportTextAllTargetsAllCultures") 207 | ); 208 | 209 | ToolbarBuilder.AddComboButton( 210 | FUIAction(), 211 | FOnGetContent::CreateRaw(this, &FTolgeeLocalizationProvider::CreateProjectSettingsWidget, InLocalizationTarget), 212 | NSLOCTEXT("Tolgee", "TolgeeSettings", "Tolgee Settings"), 213 | NSLOCTEXT("Tolgee", "TolgeeSettings", "Tolgee Settings"), 214 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "LocalizationDashboard.CompileTextAllTargetsAllCultures") 215 | ); 216 | } 217 | 218 | void FTolgeeLocalizationProvider::AddTargetSetToolbarButtons(FToolBarBuilder& ToolbarBuilder, TWeakObjectPtr InLocalizationTargetSet) 219 | { 220 | if (InLocalizationTargetSet.IsValid() && InLocalizationTargetSet->TargetObjects.Num() > 0 && InLocalizationTargetSet->TargetObjects[0]->IsMemberOfEngineTargetSet()) 221 | { 222 | return; 223 | } 224 | 225 | ToolbarBuilder.AddToolBarButton( 226 | FUIAction( 227 | FExecuteAction::CreateRaw(this, &FTolgeeLocalizationProvider::ImportAllTargetsForSetFromTolgee, InLocalizationTargetSet) 228 | ), 229 | NAME_None, 230 | NSLOCTEXT("Tolgee", "TolgeeImportSet", "Tolgee Pull"), 231 | NSLOCTEXT("Tolgee", "TolgeeImportSetTip", "Imports all targets for this set from Tolgee"), 232 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "LocalizationDashboard.ImportTextAllTargetsAllCultures") 233 | ); 234 | 235 | ToolbarBuilder.AddToolBarButton( 236 | FUIAction( 237 | FExecuteAction::CreateRaw(this, &FTolgeeLocalizationProvider::ExportAllTargetsForSetToTolgee, InLocalizationTargetSet) 238 | ), 239 | NAME_None, 240 | NSLOCTEXT("Tolgee", "TolgeeExportSet", "Tolgee Push"), 241 | NSLOCTEXT("Tolgee", "TolgeeExportSetTip", "Exports all targets for this set to Tolgee"), 242 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "LocalizationDashboard.ExportTextAllTargetsAllCultures") 243 | ); 244 | 245 | } 246 | 247 | void FTolgeeLocalizationProvider::ImportAllCulturesForTargetFromTolgee(TWeakObjectPtr LocalizationTarget) 248 | { 249 | // Delete old files if they exists so we don't accidentally export old data 250 | const FString AbsoluteFolderPath = FPaths::ConvertRelativePathToFull(GetTempSubDirectory() / LocalizationTarget->Settings.Name); 251 | IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); 252 | PlatformFile.DeleteDirectoryRecursively(*AbsoluteFolderPath); 253 | 254 | TArray CultureStats = LocalizationTarget->Settings.SupportedCulturesStatistics; 255 | FScopedSlowTask SlowTask(CultureStats.Num(), INVTEXT("Downloading Files from Localization Service...")); 256 | for (FCultureStatistics CultureStat : CultureStats) 257 | { 258 | ILocalizationServiceProvider& Provider = ILocalizationServiceModule::Get().GetProvider(); 259 | TSharedRef DownloadTargetFileOp = ILocalizationServiceOperation::Create(); 260 | DownloadTargetFileOp->SetInTargetGuid(LocalizationTarget->Settings.Guid); 261 | DownloadTargetFileOp->SetInLocale(CultureStat.CultureName); 262 | 263 | // NOTE: For some reason the base FDownloadLocalizationTargetFile prefers relative paths, so we will make it relative 264 | FString Path = AbsoluteFolderPath / CultureStat.CultureName / LocalizationTarget->Settings.Name + ".po"; 265 | FPaths::MakePathRelativeTo(Path, *FPaths::ProjectDir()); 266 | DownloadTargetFileOp->SetInRelativeOutputFilePathAndName(Path); 267 | 268 | Provider.Execute(DownloadTargetFileOp); 269 | SlowTask.EnterProgressFrame(1, FText::Format(INVTEXT("Downloading {0}"), FText::FromString(CultureStat.CultureName))); 270 | } 271 | 272 | IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); 273 | const TSharedPtr& MainFrameParentWindow = MainFrameModule.GetParentWindow(); 274 | LocalizationCommandletTasks::ImportTextForTarget(MainFrameParentWindow.ToSharedRef(), LocalizationTarget.Get(), AbsoluteFolderPath); 275 | UpdateTargetFromReports(LocalizationTarget); 276 | } 277 | 278 | void FTolgeeLocalizationProvider::ExportAllCulturesForTargetToTolgee(TWeakObjectPtr LocalizationTarget) 279 | { 280 | // Delete old files if they exists so we don't accidentally export old data 281 | const FString AbsoluteFolderPath = FPaths::ConvertRelativePathToFull(GetTempSubDirectory() / LocalizationTarget->Settings.Name); 282 | IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); 283 | PlatformFile.DeleteDirectoryRecursively(*AbsoluteFolderPath); 284 | 285 | // Currently Unreal's format uses msgctx which is not supported by Tolgee, so we will use Crowdin format instead which doesn't use it. 286 | // TODO: Revisit after https://github.com/tolgee/tolgee-platform/issues/3053 287 | LocalizationTarget->Settings.ExportSettings.POFormat = EPortableObjectFormat::Crowdin; 288 | 289 | IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); 290 | const TSharedPtr& MainFrameParentWindow = MainFrameModule.GetParentWindow(); 291 | LocalizationCommandletTasks::ExportTextForTarget(MainFrameParentWindow.ToSharedRef(), LocalizationTarget.Get(), AbsoluteFolderPath); 292 | 293 | TArray CultureStats = LocalizationTarget->Settings.SupportedCulturesStatistics; 294 | FScopedSlowTask SlowTask(CultureStats.Num(), INVTEXT("Uploading Files to Localization Service...")); 295 | for (FCultureStatistics CultureStat : CultureStats) 296 | { 297 | ILocalizationServiceProvider& Provider = ILocalizationServiceModule::Get().GetProvider(); 298 | TSharedRef UploadFileOp = ILocalizationServiceOperation::Create(); 299 | UploadFileOp->SetInTargetGuid(LocalizationTarget->Settings.Guid); 300 | UploadFileOp->SetInLocale(CultureStat.CultureName); 301 | 302 | // NOTE: For some reason the base FUploadLocalizationTargetFile prefers relative paths, so we will make it relative 303 | FString Path = AbsoluteFolderPath / CultureStat.CultureName / LocalizationTarget->Settings.Name + ".po"; 304 | FPaths::MakePathRelativeTo(Path, *FPaths::ProjectDir()); 305 | UploadFileOp->SetInRelativeInputFilePathAndName(Path); 306 | 307 | Provider.Execute(UploadFileOp); 308 | SlowTask.EnterProgressFrame(1, FText::Format(INVTEXT("Uploading {0}"), FText::FromString(CultureStat.CultureName))); 309 | } 310 | } 311 | 312 | void FTolgeeLocalizationProvider::ImportAllTargetsForSetFromTolgee(TWeakObjectPtr LocalizationTargetSet) 313 | { 314 | for (ULocalizationTarget* LocalizationTarget : LocalizationTargetSet->TargetObjects) 315 | { 316 | ImportAllCulturesForTargetFromTolgee(LocalizationTarget); 317 | } 318 | } 319 | 320 | void FTolgeeLocalizationProvider::ExportAllTargetsForSetToTolgee(TWeakObjectPtr LocalizationTargetSet) 321 | { 322 | for (ULocalizationTarget* LocalizationTarget : LocalizationTargetSet->TargetObjects) 323 | { 324 | ExportAllCulturesForTargetToTolgee(LocalizationTarget); 325 | } 326 | } 327 | 328 | TSharedRef FTolgeeLocalizationProvider::CreateProjectSettingsWidget(TWeakObjectPtr InLocalizationTarget) 329 | { 330 | const FGuid& TargetGuid = InLocalizationTarget->Settings.Guid; 331 | 332 | UTolgeeEditorSettings* MutableSettings = GetMutableDefault(); 333 | FTolgeePerTargetSettings ProjectSettings = MutableSettings->PerTargetSettings.FindOrAdd(TargetGuid); 334 | 335 | FDetailsViewArgs DetailsViewArgs; 336 | DetailsViewArgs.bAllowSearch = false; 337 | DetailsViewArgs.bShowObjectLabel = false; 338 | DetailsViewArgs.bShowScrollBar = false; 339 | 340 | FStructureDetailsViewArgs StructureViewArgs; 341 | 342 | TSharedPtr> Struct = MakeShared>(); 343 | Struct->InitializeAs(ProjectSettings); 344 | 345 | FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); 346 | TSharedPtr StructureDetailsView = PropertyEditorModule.CreateStructureDetailView(DetailsViewArgs, StructureViewArgs, Struct); 347 | 348 | StructureDetailsView->GetOnFinishedChangingPropertiesDelegate().AddLambda([MutableSettings, Struct, TargetGuid](const FPropertyChangedEvent& PropertyChangedEvent) 349 | { 350 | MutableSettings->PerTargetSettings[TargetGuid] = *Struct->Cast(); 351 | MutableSettings->SaveConfig(); 352 | }); 353 | 354 | return StructureDetailsView->GetWidget().ToSharedRef(); 355 | } 356 | 357 | void FTolgeeLocalizationProvider::UpdateTargetFromReports(TWeakObjectPtr InLocalizationTarget) 358 | { 359 | // NOTE: This function seems to be copy pasted in a lot of places FLocalizationTargetDetailCustomization/SLocalizationTargetEditorCultureRow(original), FLocalizationTargetSetDetailCustomizationm, UpdateTargetFromReports 360 | ULocalizationTarget* LocalizationTarget = InLocalizationTarget.Get(); 361 | LocalizationTarget->UpdateWordCountsFromCSV(); 362 | LocalizationTarget->UpdateStatusFromConflictReport(); 363 | } 364 | 365 | void FTolgeeLocalizationProvider::OutputCommandMessages(const FTolgeeProviderLocalizationServiceCommand& InCommand) const 366 | { 367 | FMessageLog LocalizationServiceLog("LocalizationService"); 368 | 369 | for (int32 ErrorIndex = 0; ErrorIndex < InCommand.ErrorMessages.Num(); ++ErrorIndex) 370 | { 371 | LocalizationServiceLog.Error(FText::FromString(InCommand.ErrorMessages[ErrorIndex])); 372 | } 373 | 374 | for (int32 InfoIndex = 0; InfoIndex < InCommand.InfoMessages.Num(); ++InfoIndex) 375 | { 376 | LocalizationServiceLog.Info(FText::FromString(InCommand.InfoMessages[InfoIndex])); 377 | } 378 | } 379 | 380 | TSharedPtr FTolgeeLocalizationProvider::CreateWorker(const FName& InOperationName) const 381 | { 382 | if (const FGetTolgeeProviderLocalizationServiceWorker* Operation = WorkersMap.Find(InOperationName)) 383 | { 384 | return Operation->Execute(); 385 | } 386 | 387 | return nullptr; 388 | } 389 | 390 | ELocalizationServiceOperationCommandResult::Type FTolgeeLocalizationProvider::ExecuteSynchronousCommand(FTolgeeProviderLocalizationServiceCommand& InCommand) 391 | { 392 | ELocalizationServiceOperationCommandResult::Type Result = ELocalizationServiceOperationCommandResult::Failed; 393 | 394 | // Display the progress dialog if a string was provided 395 | { 396 | // Issue the command asynchronously... 397 | IssueCommand(InCommand); 398 | 399 | // ... then wait for its completion (thus making it synchronous) 400 | while (!InCommand.bExecuteProcessed) 401 | { 402 | // Tick the command queue and update progress. 403 | Tick(); 404 | 405 | // Sleep for a bit so we don't busy-wait so much. 406 | FPlatformProcess::Sleep(0.01f); 407 | } 408 | 409 | // always do one more Tick() to make sure the command queue is cleaned up. 410 | Tick(); 411 | 412 | if (InCommand.bCommandSuccessful) 413 | { 414 | Result = ELocalizationServiceOperationCommandResult::Succeeded; 415 | } 416 | } 417 | 418 | // Delete the command now (asynchronous commands are deleted in the Tick() method) 419 | check(!InCommand.bAutoDelete); 420 | 421 | // ensure commands that are not auto deleted do not end up in the command queue 422 | if (CommandQueue.Contains(&InCommand)) 423 | { 424 | CommandQueue.Remove(&InCommand); 425 | } 426 | delete &InCommand; 427 | 428 | return Result; 429 | } 430 | 431 | ELocalizationServiceOperationCommandResult::Type FTolgeeLocalizationProvider::IssueCommand(FTolgeeProviderLocalizationServiceCommand& InCommand) 432 | { 433 | if (GThreadPool != nullptr) 434 | { 435 | // Queue this to our worker thread(s) for resolving 436 | GThreadPool->AddQueuedWork(&InCommand); 437 | CommandQueue.Add(&InCommand); 438 | return ELocalizationServiceOperationCommandResult::Succeeded; 439 | } 440 | else 441 | { 442 | FText Message(INVTEXT("There are no threads available to process the localization service provider command.")); 443 | FMessageLog("LocalizationService").Error(Message); 444 | 445 | InCommand.bCommandSuccessful = false; 446 | return InCommand.ReturnResults(); 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /Source/TolgeeProvider/Private/TolgeeProvider.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeProvider.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "TolgeeLog.h" 10 | #include "TolgeeProviderLocalizationServiceOperations.h" 11 | 12 | void FTolgeeProviderModule::StartupModule() 13 | { 14 | DenyEditorSettings(); 15 | 16 | IModularFeatures::Get().RegisterModularFeature("LocalizationService", &TolgeeLocalizationProvider); 17 | 18 | TolgeeLocalizationProvider.RegisterWorker("UploadLocalizationTargetFile"); 19 | TolgeeLocalizationProvider.RegisterWorker("DownloadLocalizationTargetFile"); 20 | 21 | // Currently, there is a bug in 5.5 where the saved settings are not getting applied 22 | // because FLocalizationServiceSettings::LoadSettings reads from the `CoalescedSourceConfigs` config 23 | ReApplySettings(); 24 | } 25 | 26 | void FTolgeeProviderModule::ShutdownModule() 27 | { 28 | IModularFeatures::Get().UnregisterModularFeature("LocalizationService", &TolgeeLocalizationProvider); 29 | } 30 | 31 | void FTolgeeProviderModule::ReApplySettings() 32 | { 33 | const FString& IniFile = LocalizationServiceHelpers::GetSettingsIni(); 34 | if (FConfigFile* ConfigFile = GConfig->Find(IniFile)) 35 | { 36 | ConfigFile->Read(IniFile); 37 | } 38 | 39 | ILocalizationServiceModule& LocalizationService = ILocalizationServiceModule::Get(); 40 | FString SavedProvider; 41 | GConfig->GetString(TEXT("LocalizationService.LocalizationServiceSettings"), TEXT("Provider"), SavedProvider, IniFile); 42 | if (!SavedProvider.IsEmpty() && LocalizationService.GetProvider().GetName() != FName(SavedProvider)) 43 | { 44 | UE_LOG(LogTolgee, Display, TEXT("Applying saved Provider: %s"), *SavedProvider); 45 | LocalizationService.SetProvider(FName(SavedProvider)); 46 | } 47 | 48 | bool bSavedUseGlobalSettings = false; 49 | const FString& GlobalIniFile = LocalizationServiceHelpers::GetGlobalSettingsIni(); 50 | GConfig->GetBool(TEXT("LocalizationService.LocalizationServiceSettings"), TEXT("UseGlobalSettings"), bSavedUseGlobalSettings, GlobalIniFile); 51 | if (LocalizationService.GetUseGlobalSettings() != bSavedUseGlobalSettings) 52 | { 53 | LocalizationService.SetUseGlobalSettings(bSavedUseGlobalSettings); 54 | } 55 | } 56 | 57 | void FTolgeeProviderModule::DenyEditorSettings() 58 | { 59 | const FString TolgeeProviderSettingsSection = TEXT("/Script/TolgeeProvider.TolgeeProviderSettings"); 60 | UProjectPackagingSettings* ProjectPackagingSettings = GetMutableDefault(); 61 | if (!ProjectPackagingSettings->IniSectionDenylist.Contains(TolgeeProviderSettingsSection)) 62 | { 63 | UE_LOG(LogTolgee, Display, TEXT("Adding %s to ProjectPackagingSettings.IniSectionDenylist"), *TolgeeProviderSettingsSection); 64 | ProjectPackagingSettings->IniSectionDenylist.Add(TolgeeProviderSettingsSection); 65 | ProjectPackagingSettings->SaveConfig(); 66 | } 67 | } 68 | 69 | IMPLEMENT_MODULE(FTolgeeProviderModule, TolgeeProvider) -------------------------------------------------------------------------------- /Source/TolgeeProvider/Private/TolgeeProviderLocalizationServiceCommand.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeProviderLocalizationServiceCommand.h" 4 | 5 | #include "TolgeeProviderLocalizationServiceWorker.h" 6 | 7 | FTolgeeProviderLocalizationServiceCommand::FTolgeeProviderLocalizationServiceCommand(const TSharedRef& InOperation, const TSharedRef& InWorker, const FLocalizationServiceOperationComplete& InOperationCompleteDelegate) 8 | : Operation(InOperation) 9 | , Worker(InWorker) 10 | , OperationCompleteDelegate(InOperationCompleteDelegate) 11 | , bExecuteProcessed(0) 12 | , bCommandSuccessful(false) 13 | , bAutoDelete(true) 14 | , Concurrency(ELocalizationServiceOperationConcurrency::Synchronous) 15 | { 16 | check(IsInGameThread()); 17 | } 18 | 19 | 20 | bool FTolgeeProviderLocalizationServiceCommand::DoWork() 21 | { 22 | bCommandSuccessful = Worker->Execute(*this); 23 | FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1); 24 | 25 | return bCommandSuccessful; 26 | } 27 | 28 | void FTolgeeProviderLocalizationServiceCommand::Abandon() 29 | { 30 | FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1); 31 | } 32 | 33 | void FTolgeeProviderLocalizationServiceCommand::DoThreadedWork() 34 | { 35 | Concurrency = ELocalizationServiceOperationConcurrency::Asynchronous; 36 | DoWork(); 37 | } 38 | 39 | ELocalizationServiceOperationCommandResult::Type FTolgeeProviderLocalizationServiceCommand::ReturnResults() 40 | { 41 | // NOTE: Everything in this class is copied from the SourceControl implementation (Other Localization providers copied it too). 42 | // Except this function where the Operation doesn't support adding messages, so we output them directly 43 | 44 | FMessageLog LocalizationServiceLog("LocalizationService"); 45 | 46 | for (FString& String : InfoMessages) 47 | { 48 | LocalizationServiceLog.Error(FText::FromString(String)); 49 | } 50 | for (FString& String : ErrorMessages) 51 | { 52 | LocalizationServiceLog.Info(FText::FromString(String)); 53 | } 54 | 55 | // run the completion delegate if we have one bound 56 | ELocalizationServiceOperationCommandResult::Type Result = bCommandSuccessful ? ELocalizationServiceOperationCommandResult::Succeeded : ELocalizationServiceOperationCommandResult::Failed; 57 | (void)OperationCompleteDelegate.ExecuteIfBound(Operation, Result); 58 | 59 | return Result; 60 | } -------------------------------------------------------------------------------- /Source/TolgeeProvider/Private/TolgeeProviderLocalizationServiceOperations.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeProviderLocalizationServiceOperations.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "TolgeeProviderLocalizationServiceCommand.h" 10 | #include "TolgeeEditorSettings.h" 11 | #include "TolgeeLog.h" 12 | #include "TolgeeProviderUtils.h" 13 | #include "TolgeeUtils.h" 14 | 15 | FName FTolgeeProviderUploadFileWorker::GetName() const 16 | { 17 | return "UploadFileWorker"; 18 | } 19 | 20 | bool FTolgeeProviderUploadFileWorker::Execute(FTolgeeProviderLocalizationServiceCommand& InCommand) 21 | { 22 | TSharedPtr UploadFileOp = StaticCastSharedRef(InCommand.Operation); 23 | if (!UploadFileOp || !UploadFileOp.IsValid()) 24 | { 25 | UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Invalid operation")); 26 | 27 | InCommand.bCommandSuccessful = false; 28 | return InCommand.bCommandSuccessful; 29 | } 30 | 31 | const FString FilePathAndName = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir() / UploadFileOp->GetInRelativeInputFilePathAndName()); 32 | FString FileContents; 33 | if (!FFileHelper::LoadFileToString(FileContents, *FilePathAndName)) 34 | { 35 | UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Cannot load file %s"), *FilePathAndName); 36 | 37 | InCommand.bCommandSuccessful = false; 38 | return InCommand.bCommandSuccessful; 39 | } 40 | 41 | const FGuid TargetGuid = UploadFileOp->GetInTargetGuid(); 42 | const FString Locale = UploadFileOp->GetInLocale(); 43 | 44 | //NOTE: This is currently not set by the Localization dashbard, when it becomes available send it as "removeOtherKeys" 45 | const bool bRemoveMissingKeys = !UploadFileOp->GetPreserveAllText(); 46 | 47 | TSharedRef FileMapping = MakeShared(); 48 | const FString FileName = FPaths::GetCleanFilename(FilePathAndName); 49 | FileMapping->SetStringField(TEXT("fileName"), FileName); 50 | 51 | TSharedRef Params = MakeShared(); 52 | Params->SetBoolField(TEXT("convertPlaceholdersToIcu"), false); 53 | Params->SetBoolField(TEXT("createNewKeys"), true); 54 | Params->SetArrayField(TEXT("fileMappings"), {MakeShared(FileMapping)}); 55 | Params->SetStringField(TEXT("forceMode"), TEXT("KEEP")); 56 | Params->SetBoolField(TEXT("overrideKeyDescriptions"), true); 57 | Params->SetBoolField(TEXT("removeOtherKeys"), true); 58 | Params->SetArrayField(TEXT("tagNewKeys"), {MakeShared(TEXT("UnrealSDK"))}); 59 | 60 | FString ParamsContents; 61 | TSharedRef> Writer = TJsonWriterFactory<>::Create(&ParamsContents); 62 | FJsonSerializer::Serialize(Params, Writer); 63 | 64 | const UTolgeeEditorSettings* ProviderSettings = GetDefault(); 65 | const FTolgeePerTargetSettings* ProjectSettings = ProviderSettings->PerTargetSettings.Find(TargetGuid); 66 | const FString Url = FString::Printf(TEXT("%s/v2/projects/%s/single-step-import"), *ProviderSettings->ApiUrl, *ProjectSettings->ProjectId); 67 | 68 | FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); 69 | HttpRequest->SetURL(Url); 70 | HttpRequest->SetVerb(TEXT("POST")); 71 | HttpRequest->SetHeader(TEXT("X-API-Key"), ProviderSettings->ApiKey); 72 | TolgeeUtils::AddSdkHeaders(HttpRequest); 73 | 74 | const FString Boundary = "---------------------------" + FString::FromInt(FDateTime::Now().GetTicks()); 75 | TolgeeProviderUtils::AddMultiRequestHeader(HttpRequest, Boundary); 76 | TolgeeProviderUtils::AddMultiRequestPart(HttpRequest, Boundary, TEXT("name=\"params\""), ParamsContents); 77 | TolgeeProviderUtils::AddMultiRequestPart(HttpRequest, Boundary, TEXT("name=\"files\"; filename=\"test.po\""), FileContents); 78 | TolgeeProviderUtils::FinishMultiRequest(HttpRequest, Boundary); 79 | 80 | HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); 81 | 82 | HttpRequest->ProcessRequestUntilComplete(); 83 | 84 | FHttpResponsePtr Response = HttpRequest->GetResponse(); 85 | if (!Response) 86 | { 87 | UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Failed to upload file %s"), *FilePathAndName); 88 | 89 | InCommand.bCommandSuccessful = false; 90 | return InCommand.bCommandSuccessful; 91 | } 92 | if (!EHttpResponseCodes::IsOk(Response->GetResponseCode())) 93 | { 94 | UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Failed to upload file %s. Response code: %d"), *FilePathAndName, Response->GetResponseCode()); 95 | UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Response: %s"), *Response->GetContentAsString()); 96 | 97 | InCommand.bCommandSuccessful = false; 98 | return InCommand.bCommandSuccessful; 99 | } 100 | 101 | UE_LOG(LogTolgee, Display, TEXT("FTolgeeProviderUploadFileWorker: Successfully uploaded file %s. Content: %s"), *FilePathAndName, *Response->GetContentAsString()); 102 | 103 | InCommand.bCommandSuccessful = true; 104 | return InCommand.bCommandSuccessful; 105 | } 106 | 107 | bool FTolgeeProviderUploadFileWorker::UpdateStates() const 108 | { 109 | return true; 110 | } 111 | 112 | FName FTolgeeProviderDownloadFileWorker::GetName() const 113 | { 114 | return "DownloadFileWorker"; 115 | } 116 | 117 | bool FTolgeeProviderDownloadFileWorker::Execute(FTolgeeProviderLocalizationServiceCommand& InCommand) 118 | { 119 | TSharedPtr DownloadFileOp = StaticCastSharedRef(InCommand.Operation); 120 | if (!DownloadFileOp || !DownloadFileOp.IsValid()) 121 | { 122 | UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderDownloadFileWorker: Invalid operation")); 123 | 124 | InCommand.bCommandSuccessful = false; 125 | return InCommand.bCommandSuccessful; 126 | } 127 | 128 | const FString FilePathAndName = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir() / DownloadFileOp->GetInRelativeOutputFilePathAndName()); 129 | const FGuid TargetGuid = DownloadFileOp->GetInTargetGuid(); 130 | const FString Locale = DownloadFileOp->GetInLocale(); 131 | 132 | const UTolgeeEditorSettings* ProviderSettings = GetDefault(); 133 | const FTolgeePerTargetSettings* ProjectSettings = ProviderSettings->PerTargetSettings.Find(TargetGuid); 134 | 135 | const FString Url = FString::Printf(TEXT("%s/v2/projects/%s/export"), *ProviderSettings->ApiUrl, *ProjectSettings->ProjectId); 136 | 137 | FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); 138 | HttpRequest->SetURL(Url); 139 | HttpRequest->SetVerb(TEXT("POST")); 140 | HttpRequest->SetHeader(TEXT("X-API-Key"), ProviderSettings->ApiKey); 141 | HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); 142 | TolgeeUtils::AddSdkHeaders(HttpRequest); 143 | 144 | TSharedRef Body = MakeShared(); 145 | Body->SetBoolField(TEXT("escapeHtml"), false); 146 | Body->SetStringField(TEXT("format"), "PO"); 147 | Body->SetBoolField(TEXT("supportArrays"), false); 148 | Body->SetBoolField(TEXT("zip"), false); 149 | Body->SetArrayField(TEXT("languages"), {MakeShared(Locale)}); 150 | 151 | FString RequestBody; 152 | TSharedRef> Writer = TJsonWriterFactory<>::Create(&RequestBody); 153 | FJsonSerializer::Serialize(Body, Writer); 154 | 155 | HttpRequest->SetContentAsString(RequestBody); 156 | HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread); 157 | 158 | HttpRequest->ProcessRequestUntilComplete(); 159 | 160 | FHttpResponsePtr Response = HttpRequest->GetResponse(); 161 | if (!Response) 162 | { 163 | UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Failed to download file %s"), *FilePathAndName); 164 | 165 | InCommand.bCommandSuccessful = false; 166 | return InCommand.bCommandSuccessful; 167 | } 168 | if (!EHttpResponseCodes::IsOk(Response->GetResponseCode())) 169 | { 170 | UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Failed to download file %s. Response code: %d"), *FilePathAndName, Response->GetResponseCode()); 171 | UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderUploadFileWorker: Response: %s"), *Response->GetContentAsString()); 172 | 173 | InCommand.bCommandSuccessful = false; 174 | return InCommand.bCommandSuccessful; 175 | } 176 | 177 | if (!FFileHelper::SaveStringToFile(Response->GetContentAsString(), *FilePathAndName, FFileHelper::EEncodingOptions::ForceUnicode)) 178 | { 179 | UE_LOG(LogTolgee, Error, TEXT("FTolgeeProviderDownloadFileWorker: Failed to write file %s"), *FilePathAndName); 180 | 181 | InCommand.bCommandSuccessful = false; 182 | return InCommand.bCommandSuccessful; 183 | } 184 | 185 | UE_LOG(LogTolgee, Display, TEXT("FTolgeeProviderUploadFileWorker: Successfully downloaded file %s. Content: %s"), *FilePathAndName, *Response->GetContentAsString()); 186 | 187 | InCommand.bCommandSuccessful = true; 188 | return InCommand.bCommandSuccessful; 189 | } 190 | 191 | bool FTolgeeProviderDownloadFileWorker::UpdateStates() const 192 | { 193 | return true; 194 | } -------------------------------------------------------------------------------- /Source/TolgeeProvider/Private/TolgeeProviderUtils.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #include "TolgeeProviderUtils.h" 4 | 5 | void TolgeeProviderUtils::AddMultiRequestHeader(const FHttpRequestRef& HttpRequest, const FString& Boundary) 6 | { 7 | HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("multipart/form-data; boundary =" + Boundary)); 8 | } 9 | 10 | void TolgeeProviderUtils::AddMultiRequestPart(const FHttpRequestRef& HttpRequest, const FString& Boundary, const FString& ExtraHeaders, const FString& Value) 11 | { 12 | const FString BoundaryBegin = FString(TEXT("--")) + Boundary + FString(TEXT("\r\n")); 13 | 14 | FString Data = GetRequestContent(HttpRequest); 15 | Data += FString(TEXT("\r\n")) 16 | + BoundaryBegin 17 | + FString(TEXT("Content-Disposition: form-data;")) 18 | + ExtraHeaders 19 | + FString(TEXT("\r\n\r\n")) 20 | + Value; 21 | 22 | HttpRequest->SetContentAsString(Data); 23 | } 24 | 25 | void TolgeeProviderUtils::FinishMultiRequest(const FHttpRequestRef& HttpRequest, const FString& Boundary) 26 | { 27 | const FString BoundaryEnd = FString(TEXT("\r\n--")) + Boundary + FString(TEXT("--\r\n")); 28 | 29 | FString Data = GetRequestContent(HttpRequest); 30 | Data.Append(BoundaryEnd); 31 | 32 | HttpRequest->SetContentAsString(Data); 33 | } 34 | 35 | FString TolgeeProviderUtils::GetRequestContent(const FHttpRequestRef& HttpRequest) 36 | { 37 | TArray Content = HttpRequest->GetContent(); 38 | 39 | FString Result; 40 | FFileHelper::BufferToString(Result, Content.GetData(), Content.Num()); 41 | 42 | return Result; 43 | } -------------------------------------------------------------------------------- /Source/TolgeeProvider/Public/TolgeeLocalizationProvider.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "TolgeeProviderLocalizationServiceWorker.h" 6 | 7 | #include 8 | 9 | class FTolgeeProviderLocalizationServiceCommand; 10 | 11 | DECLARE_DELEGATE_RetVal(FTolgeeProviderLocalizationServiceWorkerRef, FGetTolgeeProviderLocalizationServiceWorker) 12 | 13 | class FTolgeeLocalizationProvider final : public ILocalizationServiceProvider 14 | { 15 | public: 16 | // ~Begin ILocalizationServiceProvider interface 17 | virtual void Init(bool bForceConnection = true) override; 18 | virtual void Close() override; 19 | virtual const FName& GetName() const override; 20 | virtual const FText GetDisplayName() const override; 21 | virtual FText GetStatusText() const override; 22 | virtual bool IsEnabled() const override; 23 | virtual bool IsAvailable() const override; 24 | virtual ELocalizationServiceOperationCommandResult::Type GetState(const TArray& InTranslationIds, TArray>& OutState, ELocalizationServiceCacheUsage::Type InStateCacheUsage) override; 25 | virtual ELocalizationServiceOperationCommandResult::Type Execute(const TSharedRef& InOperation, const TArray& InTranslationIds, ELocalizationServiceOperationConcurrency::Type InConcurrency = ELocalizationServiceOperationConcurrency::Synchronous, const FLocalizationServiceOperationComplete& InOperationCompleteDelegate = FLocalizationServiceOperationComplete()) override; 26 | virtual bool CanCancelOperation(const TSharedRef& InOperation) const override; 27 | virtual void CancelOperation(const TSharedRef& InOperation) override; 28 | virtual void Tick() override; 29 | virtual void CustomizeSettingsDetails(IDetailCategoryBuilder& DetailCategoryBuilder) const override; 30 | virtual void CustomizeTargetDetails(IDetailCategoryBuilder& DetailCategoryBuilder, TWeakObjectPtr LocalizationTarget) const override; 31 | virtual void CustomizeTargetToolbar(TSharedRef& MenuExtender, TWeakObjectPtr LocalizationTarget) const override; 32 | virtual void CustomizeTargetSetToolbar(TSharedRef& MenuExtender, TWeakObjectPtr LocalizationTargetSet) const override; 33 | // ~End ILocalizationServiceProvider interface 34 | 35 | /** 36 | * Add the buttons to the toolbar for this localization target 37 | */ 38 | void AddTargetToolbarButtons(FToolBarBuilder& ToolbarBuilder, TWeakObjectPtr InLocalizationTarget); 39 | /** 40 | * Add the buttons to the toolbar for this set of localization targets 41 | */ 42 | void AddTargetSetToolbarButtons(FToolBarBuilder& ToolbarBuilder, TWeakObjectPtr InLocalizationTargetSet); 43 | /** 44 | * Download and import all translations for all cultures for the specified target from Tolgee 45 | */ 46 | void ImportAllCulturesForTargetFromTolgee(TWeakObjectPtr LocalizationTarget); 47 | /** 48 | * Export and upload all cultures for a localization target to Tolgee 49 | */ 50 | void ExportAllCulturesForTargetToTolgee(TWeakObjectPtr LocalizationTarget); 51 | /** 52 | * Download and import all translations for all cultures for all targets for the specified target set from Tolgee 53 | */ 54 | void ImportAllTargetsForSetFromTolgee(TWeakObjectPtr LocalizationTargetSet); 55 | /** 56 | * Export and upload all cultures for all targets for a localization target set to Tolgee 57 | */ 58 | void ExportAllTargetsForSetToTolgee(TWeakObjectPtr LocalizationTargetSet); 59 | /** 60 | * Create a widget to configure the target's settings. 61 | * NOTE: This spawns a widget to edit the matching sub-property of the provider settings 62 | */ 63 | TSharedRef CreateProjectSettingsWidget(TWeakObjectPtr InLocalizationTarget); 64 | /** 65 | * Refreshes the localization dashboard UI, specifically the word counter 66 | */ 67 | void UpdateTargetFromReports(TWeakObjectPtr InLocalizationTarget); 68 | /** 69 | * Sends messages from the command to the output log 70 | */ 71 | void OutputCommandMessages(const FTolgeeProviderLocalizationServiceCommand& InCommand) const; 72 | /** 73 | * Helper function to register a worker type with this provider 74 | */ 75 | template 76 | void RegisterWorker(const FName& InName); 77 | /** 78 | * Create work instances for the specified operation 79 | */ 80 | TSharedPtr CreateWorker(const FName& InOperationName) const; 81 | /** 82 | * Execute the command synchronously 83 | */ 84 | ELocalizationServiceOperationCommandResult::Type ExecuteSynchronousCommand(FTolgeeProviderLocalizationServiceCommand& InCommand); 85 | /** 86 | * Execute the command asynchronously 87 | */ 88 | ELocalizationServiceOperationCommandResult::Type IssueCommand(FTolgeeProviderLocalizationServiceCommand& InCommand); 89 | 90 | TMap WorkersMap; 91 | 92 | TArray CommandQueue; 93 | }; 94 | 95 | template 96 | void FTolgeeLocalizationProvider::RegisterWorker(const FName& InName) 97 | { 98 | FGetTolgeeProviderLocalizationServiceWorker Delegate = FGetTolgeeProviderLocalizationServiceWorker::CreateLambda([]() 99 | { 100 | return MakeShared(); 101 | }); 102 | 103 | WorkersMap.Add(InName, Delegate); 104 | } -------------------------------------------------------------------------------- /Source/TolgeeProvider/Public/TolgeeProvider.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | #include "TolgeeLocalizationProvider.h" 8 | 9 | /** 10 | * Module responsible for introducing the Tolgee implementation of the LocalizationServiceProvider. 11 | */ 12 | class FTolgeeProviderModule : public IModuleInterface 13 | { 14 | // ~Begin IModuleInterface interface 15 | virtual void StartupModule() override; 16 | virtual void ShutdownModule() override; 17 | // ~End IModuleInterface interface 18 | 19 | /** 20 | * Re-applys the settings from LocalizationService.LocalizationServiceSettings ini file. 21 | */ 22 | void ReApplySettings(); 23 | /** 24 | * Ensures editor settings are not packaged in the final game. 25 | */ 26 | void DenyEditorSettings(); 27 | /** 28 | * LocalizationServiceProvider instance for Tolgee. 29 | */ 30 | FTolgeeLocalizationProvider TolgeeLocalizationProvider; 31 | }; -------------------------------------------------------------------------------- /Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceCommand.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | 8 | class ITolgeeProviderLocalizationServiceWorker; 9 | 10 | class FTolgeeProviderLocalizationServiceCommand : public IQueuedWork 11 | { 12 | public: 13 | FTolgeeProviderLocalizationServiceCommand(const TSharedRef& InOperation, const TSharedRef& InWorker, const FLocalizationServiceOperationComplete& InOperationCompleteDelegate = FLocalizationServiceOperationComplete()); 14 | 15 | /** 16 | * This function handles the core threaded operations. 17 | * All tasks associated with this queued object should be executed within this method. 18 | */ 19 | bool DoWork(); 20 | 21 | /** 22 | * Notifies queued work of abandonment for cleanup. Invoked only if abandoned before completion. 23 | * The object must delete itself using its allocated heap. 24 | */ 25 | virtual void Abandon() override; 26 | 27 | /** 28 | * Used to signal the object to clean up, but only after it has completed its work. 29 | */ 30 | virtual void DoThreadedWork() override; 31 | 32 | /** 33 | * Save any results and call any registered callbacks. 34 | */ 35 | ELocalizationServiceOperationCommandResult::Type ReturnResults(); 36 | 37 | /** 38 | * Operation we want to perform - contains outward-facing parameters & results 39 | */ 40 | TSharedRef Operation; 41 | 42 | /** 43 | * The object that will actually do the work 44 | */ 45 | TSharedRef Worker; 46 | 47 | /** 48 | * Delegate to notify when this operation completes 49 | */ 50 | FLocalizationServiceOperationComplete OperationCompleteDelegate; 51 | 52 | /** 53 | * If true, this command has been processed by the Localization service thread 54 | */ 55 | volatile int32 bExecuteProcessed; 56 | 57 | /** 58 | * If true, the Localization service command succeeded 59 | */ 60 | bool bCommandSuccessful; 61 | 62 | /** 63 | * If true, this command will be automatically cleaned up in Tick() 64 | */ 65 | bool bAutoDelete; 66 | 67 | /** 68 | * Whether we are running multi-treaded or not 69 | */ 70 | ELocalizationServiceOperationConcurrency::Type Concurrency; 71 | 72 | /** 73 | * Info and/or warning message storage 74 | */ 75 | TArray InfoMessages; 76 | 77 | /** 78 | * Potential error message storage 79 | */ 80 | TArray ErrorMessages; 81 | }; -------------------------------------------------------------------------------- /Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceOperations.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "TolgeeProviderLocalizationServiceWorker.h" 6 | 7 | class FTolgeeProviderUploadFileWorker : public ITolgeeProviderLocalizationServiceWorker 8 | { 9 | // ~Begin ITolgeeProviderLocalizationServiceWorker 10 | virtual FName GetName() const override; 11 | virtual bool Execute(FTolgeeProviderLocalizationServiceCommand& InCommand) override; 12 | virtual bool UpdateStates() const override; 13 | // ~End ITolgeeProviderLocalizationServiceWorker 14 | }; 15 | 16 | class FTolgeeProviderDownloadFileWorker : public ITolgeeProviderLocalizationServiceWorker 17 | { 18 | // ~Begin ITolgeeProviderLocalizationServiceWorker 19 | virtual FName GetName() const override; 20 | virtual bool Execute(FTolgeeProviderLocalizationServiceCommand& InCommand) override; 21 | virtual bool UpdateStates() const override; 22 | // ~End ITolgeeProviderLocalizationServiceWorker 23 | }; -------------------------------------------------------------------------------- /Source/TolgeeProvider/Public/TolgeeProviderLocalizationServiceWorker.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | class FTolgeeProviderLocalizationServiceCommand; 6 | 7 | /** 8 | * Base class for worker implementation that will be run to perform the commands. 9 | */ 10 | class ITolgeeProviderLocalizationServiceWorker 11 | { 12 | public: 13 | virtual ~ITolgeeProviderLocalizationServiceWorker() = default; 14 | 15 | /** 16 | * Used to uniquely identify the worker type. 17 | */ 18 | virtual FName GetName() const = 0; 19 | /** 20 | * Used to perform any work that is necessary to complete the command. 21 | * NOTE: Make sure you block the execution until the command is completed. 22 | */ 23 | virtual bool Execute(FTolgeeProviderLocalizationServiceCommand& InCommand) = 0; 24 | /** 25 | * Used to update the state of localization items after the command is completed. 26 | * NOTE: This will always run on the main thread. 27 | */ 28 | virtual bool UpdateStates() const = 0; 29 | }; 30 | 31 | using FTolgeeProviderLocalizationServiceWorkerRef = TSharedRef; -------------------------------------------------------------------------------- /Source/TolgeeProvider/Public/TolgeeProviderUtils.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | namespace TolgeeProviderUtils 8 | { 9 | /** 10 | * Adds the multi-part request header to the HTTP request. 11 | */ 12 | void AddMultiRequestHeader(const FHttpRequestRef& HttpRequest, const FString& Boundary); 13 | /** 14 | * Adds a multi-part request part to the HTTP with the given content based on the boundary and extra headers. 15 | */ 16 | void AddMultiRequestPart(const FHttpRequestRef& HttpRequest, const FString& Boundary, const FString& ExtraHeaders, const FString& Value); 17 | /** 18 | * Finishes the multi-part request by appending the boundary end to the HTTP request. 19 | */ 20 | void FinishMultiRequest(const FHttpRequestRef& HttpRequest, const FString& Boundary); 21 | /** 22 | * Gets the request content from the HTTP request as a string. 23 | */ 24 | FString GetRequestContent(const FHttpRequestRef& HttpRequest); 25 | } -------------------------------------------------------------------------------- /Source/TolgeeProvider/TolgeeProvider.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tolgee 2022-2025. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class TolgeeProvider : ModuleRules 6 | { 7 | public TolgeeProvider(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PublicDependencyModuleNames.AddRange( 10 | new string[] 11 | { 12 | "Core", 13 | "CoreUObject", 14 | "DeveloperToolSettings", 15 | "DeveloperSettings", 16 | "Engine", 17 | "HTTP", 18 | "Json", 19 | "Localization", 20 | "LocalizationCommandletExecution", 21 | "LocalizationService", 22 | "Slate", 23 | "SlateCore", 24 | 25 | "Tolgee", 26 | "TolgeeEditor", 27 | } 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /Tolgee.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "VersionName": "0.0", 4 | "EngineVersion": "5.5", 5 | "FriendlyName": "Tolgee", 6 | "Description": "Localization tool which makes the localization process simple. Easy to integrate to React, Angular and other applications.", 7 | "Category": "Localization", 8 | "CreatedBy": "Tolgee", 9 | "CreatedByURL": "https://tolgee.io/", 10 | "DocsURL": "https://tolgee.io/integrations/unreal", 11 | "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/2757e202f8f3408bbf66f65d26223398", 12 | "SupportURL": "https://tolg.ee/slack", 13 | "EnabledByDefault": false, 14 | "CanContainContent": false, 15 | "Modules": [ 16 | { 17 | "Name": "Tolgee", 18 | "Type": "Runtime", 19 | "LoadingPhase": "Default", 20 | "WhitelistPlatforms": [ 21 | "Android", 22 | "IOS", 23 | "Linux", 24 | "Mac", 25 | "TVOS", 26 | "Win64" 27 | ] 28 | }, 29 | { 30 | "Name": "TolgeeEditor", 31 | "Type": "Editor", 32 | "LoadingPhase": "Default", 33 | "WhitelistPlatforms": [ 34 | "Linux", 35 | "Mac", 36 | "Win64" 37 | ] 38 | }, 39 | { 40 | "Name": "TolgeeProvider", 41 | "Type": "Editor", 42 | "LoadingPhase": "Default", 43 | "WhitelistPlatforms": [ 44 | "Win64", 45 | "Mac", 46 | "Linux" 47 | ] 48 | } 49 | ], 50 | "Plugins": [ 51 | { 52 | "Name": "WebBrowserWidget", 53 | "Enabled": true 54 | } 55 | ] 56 | } --------------------------------------------------------------------------------