Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ BreakBeforeBraces: Custom
BinPackArguments: false
BinPackParameters: false
BreakConstructorInitializers: AfterColon
ColumnLimit: 200
ColumnLimit: 1000
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '.*\.generated\.h'
Expand Down
2 changes: 1 addition & 1 deletion Source/Tolgee/Private/Tolgee.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Tolgee 2022-2023. All Rights Reserved.
// Copyright (c) Tolgee 2022-2025. All Rights Reserved.

#include "Tolgee.h"

Expand Down
12 changes: 0 additions & 12 deletions Source/Tolgee/Private/Tolgee.h

This file was deleted.

118 changes: 118 additions & 0 deletions Source/Tolgee/Private/TolgeeCdnFetcherSubsystem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) Tolgee 2022-2025. All Rights Reserved.

#include "TolgeeCdnFetcherSubsystem.h"

#include <HttpModule.h>
#include <Interfaces/IHttpResponse.h>
#include <Kismet/KismetInternationalizationLibrary.h>

#include "TolgeeRuntimeSettings.h"
#include "TolgeeLog.h"

void UTolgeeCdnFetcherSubsystem::OnGameInstanceStart(UGameInstance* GameInstance)
{
const UTolgeeRuntimeSettings* Settings = GetDefault<UTolgeeRuntimeSettings>();
if (Settings->CdnAddresses.IsEmpty())
{
UE_LOG(LogTolgee, Display, TEXT("No CDN addresses configured. Packaged builds will only use static data."));
return;
}
if (GIsEditor && !Settings->bUseCdnInEditor)
{
UE_LOG(LogTolgee, Display, TEXT("CDN was disabled for editor but it will be used in the final game."));
return;
}

FetchAllCdns();
}

void UTolgeeCdnFetcherSubsystem::OnGameInstanceEnd(bool bIsSimulating)
{
ResetData();

LastModifiedDates.Empty();
}

TMap<FString, TArray<FTolgeeTranslationData>> UTolgeeCdnFetcherSubsystem::GetDataToInject() const
{
return CachedTranslations;
}

void UTolgeeCdnFetcherSubsystem::FetchAllCdns()
{
const UTolgeeRuntimeSettings* Settings = GetDefault<UTolgeeRuntimeSettings>();

for (const FString& CdnAddress : Settings->CdnAddresses)
{
TArray<FString> Cultures = UKismetInternationalizationLibrary::GetLocalizedCultures();
for (const FString& Culture : Cultures)
{
const FString DownloadUrl = FString::Printf(TEXT("%s/%s.po"), *CdnAddress, *Culture);

UE_LOG(LogTolgee, Display, TEXT("Fetching localization data for culture: %s from CDN: %s"), *Culture, *DownloadUrl);
FetchFromCdn(Culture, DownloadUrl);
}
};

}

void UTolgeeCdnFetcherSubsystem::FetchFromCdn(const FString& Culture, const FString& DownloadUrl)
{
const FString* LastModifiedDate = LastModifiedDates.Find(DownloadUrl);

const FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest();
HttpRequest->SetVerb("GET");
HttpRequest->SetURL(DownloadUrl);
HttpRequest->SetHeader(TEXT("accept"), TEXT("application/json"));

if (LastModifiedDate)
{
HttpRequest->SetHeader(TEXT("If-Modified-Since"), *LastModifiedDate);
}

HttpRequest->OnProcessRequestComplete().BindUObject(this, &ThisClass::OnFetchedFromCdn, Culture);
HttpRequest->ProcessRequest();

NumRequestsSent++;
}

void UTolgeeCdnFetcherSubsystem::OnFetchedFromCdn(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FString InCutlure)
{
if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode()))
{
UE_LOG(LogTolgee, Display, TEXT("Fetch successfully for %s to %s."), *InCutlure, *Request->GetURL());

TArray<FTolgeeTranslationData> Translations = ExtractTranslationsFromPO(Response->GetContentAsString());
CachedTranslations.Emplace(InCutlure, Translations);

const FString LastModified = Response->GetHeader(TEXT("Last-Modified"));
if (!LastModified.IsEmpty())
{
LastModifiedDates.Emplace(Request->GetURL(), LastModified);
}
}
else if (Response.IsValid() && Response->GetResponseCode() == EHttpResponseCodes::NotModified)
{
UE_LOG(LogTolgee, Display, TEXT("No new data for %s to %s."), *InCutlure, *Request->GetURL());
}
else
{
UE_LOG(LogTolgee, Error, TEXT("Request for %s to %s failed."), *InCutlure, *Request->GetURL());
}

NumRequestsCompleted++;

if (NumRequestsCompleted == NumRequestsSent)
{
UE_LOG(LogTolgee, Display, TEXT("All requests completed. Refreshing translation data."));
RefreshTranslationDataAsync();
}
}

void UTolgeeCdnFetcherSubsystem::ResetData()
{
NumRequestsSent = 0;
NumRequestsCompleted = 0;

CachedTranslations.Empty();
}
124 changes: 124 additions & 0 deletions Source/Tolgee/Private/TolgeeLocalizationInjectorSubsystem.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) Tolgee 2022-2025. All Rights Reserved.

#include "TolgeeLocalizationInjectorSubsystem.h"

#include <Async/Async.h>
#include <Engine/World.h>
#include <Internationalization/TextLocalizationResource.h>

#if WITH_LOCALIZATION_MODULE
#include <PortableObjectFormatDOM.h>
#include <PortableObjectPipeline.h>
#endif

#include "TolgeeLog.h"
#include "TolgeeTextSource.h"

void UTolgeeLocalizationInjectorSubsystem::OnGameInstanceStart(UGameInstance* GameInstance)
{
}

void UTolgeeLocalizationInjectorSubsystem::OnGameInstanceEnd(bool bIsSimulating)
{
}

void UTolgeeLocalizationInjectorSubsystem::GetLocalizedResources(const ELocalizationLoadFlags InLoadFlags, TArrayView<const FString> InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource) const
{
TMap<FString, TArray<FTolgeeTranslationData>> DataToInject = GetDataToInject();
for (const TPair<FString, TArray<FTolgeeTranslationData>>& CachedTranslation : DataToInject)
{
if (!InPrioritizedCultures.Contains(CachedTranslation.Key))
{
continue;
}

for (const FTolgeeTranslationData& TranslationData : CachedTranslation.Value)
{
const FTextKey InNamespace = TranslationData.ParsedNamespace;
const FTextKey InKey = TranslationData.ParsedKey;
const FString InLocalizedString = TranslationData.Translation;

if (FTextLocalizationResource::FEntry* ExistingEntry = InOutLocalizedResource.Entries.Find(FTextId(InNamespace, InKey)))
{
//NOTE: -1 is a higher than usual priority, meaning this entry will override any existing one. See FTextLocalizationResource::ShouldReplaceEntry
InOutLocalizedResource.AddEntry(InNamespace, InKey, ExistingEntry->SourceStringHash, InLocalizedString, -1);
}
else
{
UE_LOG(LogTolgee, Warning, TEXT("Failed to inject translation for %s:%s. Default entry not found."), *InNamespace.ToString(), *InKey.ToString());
}
}
}
}

TMap<FString, TArray<FTolgeeTranslationData>> UTolgeeLocalizationInjectorSubsystem::GetDataToInject() const
{
return {};
}

void UTolgeeLocalizationInjectorSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);

TextSource = MakeShared<FTolgeeTextSource>();
TextSource->GetLocalizedResources.BindUObject(this, &ThisClass::GetLocalizedResources);
FTextLocalizationManager::Get().RegisterTextSource(TextSource.ToSharedRef());

FWorldDelegates::OnStartGameInstance.AddUObject(this, &ThisClass::OnGameInstanceStart);

#if WITH_EDITOR
FEditorDelegates::PrePIEEnded.AddUObject(this, &ThisClass::OnGameInstanceEnd);
#endif
}

void UTolgeeLocalizationInjectorSubsystem::RefreshTranslationDataAsync()
{
UE_LOG(LogTolgee, Verbose, TEXT("RefreshTranslationDataAsync requested."));

AsyncTask(
ENamedThreads::AnyHiPriThreadHiPriTask,
[=]()
{
TRACE_CPUPROFILER_EVENT_SCOPE(UTolgeeLocalizationInjectorSubsystem::RefreshResources)

UE_LOG(LogTolgee, Verbose, TEXT("RefreshTranslationDataAsync executing."));

FTextLocalizationManager::Get().RefreshResources();
}
);
}

TArray<FTolgeeTranslationData> UTolgeeLocalizationInjectorSubsystem::ExtractTranslationsFromPO(const FString& PoContent)
{
TRACE_CPUPROFILER_EVENT_SCOPE(UTolgeeLocalizationInjectorSubsystem::ExtractTranslationsFromPO)

TArray<FTolgeeTranslationData> Result;

#if WITH_LOCALIZATION_MODULE
FPortableObjectFormatDOM PortableObject;
PortableObject.FromString(PoContent);

for (auto EntryPairIter = PortableObject.GetEntriesIterator(); EntryPairIter; ++EntryPairIter)
{
auto POEntry = EntryPairIter->Value;
if (POEntry->MsgId.IsEmpty() || POEntry->MsgStr.Num() == 0 || POEntry->MsgStr[0].IsEmpty())
{
// We ignore the header entry or entries with no translation.
continue;
}

FTolgeeTranslationData TranslationData;

constexpr ELocalizedTextCollapseMode InTextCollapseMode = ELocalizedTextCollapseMode::IdenticalTextIdAndSource;
constexpr EPortableObjectFormat InPOFormat = EPortableObjectFormat::Crowdin;

PortableObjectPipeline::ParseBasicPOFileEntry(*POEntry, TranslationData.ParsedNamespace, TranslationData.ParsedKey, TranslationData.SourceText, TranslationData.Translation, InTextCollapseMode, InPOFormat);

Result.Add(TranslationData);
}
#else
UE_LOG(LogTolgee, Error, TEXT("Localization module is not available. Cannot extract translations from PO content."));
#endif

return Result;
}
Loading