Skip to content

Commit 4de22fe

Browse files
authored
Merge pull request #29 from tolgee/feature/tolgee-localization-provider
Tolgee implementation for LocalizationServiceProvider
2 parents 777717f + 4cd47e8 commit 4de22fe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2260
-2281
lines changed

.clang-format

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ BreakBeforeBraces: Custom
3232
BinPackArguments: false
3333
BinPackParameters: false
3434
BreakConstructorInitializers: AfterColon
35-
ColumnLimit: 200
35+
ColumnLimit: 1000
3636
IncludeBlocks: Regroup
3737
IncludeCategories:
3838
- Regex: '.*\.generated\.h'

Source/Tolgee/Private/Tolgee.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Tolgee 2022-2023. All Rights Reserved.
1+
// Copyright (c) Tolgee 2022-2025. All Rights Reserved.
22

33
#include "Tolgee.h"
44

Source/Tolgee/Private/Tolgee.h

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright (c) Tolgee 2022-2025. All Rights Reserved.
2+
3+
#include "TolgeeCdnFetcherSubsystem.h"
4+
5+
#include <HttpModule.h>
6+
#include <Interfaces/IHttpResponse.h>
7+
#include <Kismet/KismetInternationalizationLibrary.h>
8+
9+
#include "TolgeeRuntimeSettings.h"
10+
#include "TolgeeLog.h"
11+
12+
void UTolgeeCdnFetcherSubsystem::OnGameInstanceStart(UGameInstance* GameInstance)
13+
{
14+
const UTolgeeRuntimeSettings* Settings = GetDefault<UTolgeeRuntimeSettings>();
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<FString, TArray<FTolgeeTranslationData>> UTolgeeCdnFetcherSubsystem::GetDataToInject() const
37+
{
38+
return CachedTranslations;
39+
}
40+
41+
void UTolgeeCdnFetcherSubsystem::FetchAllCdns()
42+
{
43+
const UTolgeeRuntimeSettings* Settings = GetDefault<UTolgeeRuntimeSettings>();
44+
45+
for (const FString& CdnAddress : Settings->CdnAddresses)
46+
{
47+
TArray<FString> 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<FTolgeeTranslationData> 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+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) Tolgee 2022-2025. All Rights Reserved.
2+
3+
#include "TolgeeLocalizationInjectorSubsystem.h"
4+
5+
#include <Async/Async.h>
6+
#include <Engine/World.h>
7+
#include <Internationalization/TextLocalizationResource.h>
8+
9+
#if WITH_LOCALIZATION_MODULE
10+
#include <PortableObjectFormatDOM.h>
11+
#include <PortableObjectPipeline.h>
12+
#endif
13+
14+
#include "TolgeeLog.h"
15+
#include "TolgeeTextSource.h"
16+
17+
void UTolgeeLocalizationInjectorSubsystem::OnGameInstanceStart(UGameInstance* GameInstance)
18+
{
19+
}
20+
21+
void UTolgeeLocalizationInjectorSubsystem::OnGameInstanceEnd(bool bIsSimulating)
22+
{
23+
}
24+
25+
void UTolgeeLocalizationInjectorSubsystem::GetLocalizedResources(const ELocalizationLoadFlags InLoadFlags, TArrayView<const FString> InPrioritizedCultures, FTextLocalizationResource& InOutNativeResource, FTextLocalizationResource& InOutLocalizedResource) const
26+
{
27+
TMap<FString, TArray<FTolgeeTranslationData>> DataToInject = GetDataToInject();
28+
for (const TPair<FString, TArray<FTolgeeTranslationData>>& CachedTranslation : DataToInject)
29+
{
30+
if (!InPrioritizedCultures.Contains(CachedTranslation.Key))
31+
{
32+
continue;
33+
}
34+
35+
for (const FTolgeeTranslationData& TranslationData : CachedTranslation.Value)
36+
{
37+
const FTextKey InNamespace = TranslationData.ParsedNamespace;
38+
const FTextKey InKey = TranslationData.ParsedKey;
39+
const FString InLocalizedString = TranslationData.Translation;
40+
41+
if (FTextLocalizationResource::FEntry* ExistingEntry = InOutLocalizedResource.Entries.Find(FTextId(InNamespace, InKey)))
42+
{
43+
//NOTE: -1 is a higher than usual priority, meaning this entry will override any existing one. See FTextLocalizationResource::ShouldReplaceEntry
44+
InOutLocalizedResource.AddEntry(InNamespace, InKey, ExistingEntry->SourceStringHash, InLocalizedString, -1);
45+
}
46+
else
47+
{
48+
UE_LOG(LogTolgee, Warning, TEXT("Failed to inject translation for %s:%s. Default entry not found."), *InNamespace.ToString(), *InKey.ToString());
49+
}
50+
}
51+
}
52+
}
53+
54+
TMap<FString, TArray<FTolgeeTranslationData>> UTolgeeLocalizationInjectorSubsystem::GetDataToInject() const
55+
{
56+
return {};
57+
}
58+
59+
void UTolgeeLocalizationInjectorSubsystem::Initialize(FSubsystemCollectionBase& Collection)
60+
{
61+
Super::Initialize(Collection);
62+
63+
TextSource = MakeShared<FTolgeeTextSource>();
64+
TextSource->GetLocalizedResources.BindUObject(this, &ThisClass::GetLocalizedResources);
65+
FTextLocalizationManager::Get().RegisterTextSource(TextSource.ToSharedRef());
66+
67+
FWorldDelegates::OnStartGameInstance.AddUObject(this, &ThisClass::OnGameInstanceStart);
68+
69+
#if WITH_EDITOR
70+
FEditorDelegates::PrePIEEnded.AddUObject(this, &ThisClass::OnGameInstanceEnd);
71+
#endif
72+
}
73+
74+
void UTolgeeLocalizationInjectorSubsystem::RefreshTranslationDataAsync()
75+
{
76+
UE_LOG(LogTolgee, Verbose, TEXT("RefreshTranslationDataAsync requested."));
77+
78+
AsyncTask(
79+
ENamedThreads::AnyHiPriThreadHiPriTask,
80+
[=]()
81+
{
82+
TRACE_CPUPROFILER_EVENT_SCOPE(UTolgeeLocalizationInjectorSubsystem::RefreshResources)
83+
84+
UE_LOG(LogTolgee, Verbose, TEXT("RefreshTranslationDataAsync executing."));
85+
86+
FTextLocalizationManager::Get().RefreshResources();
87+
}
88+
);
89+
}
90+
91+
TArray<FTolgeeTranslationData> UTolgeeLocalizationInjectorSubsystem::ExtractTranslationsFromPO(const FString& PoContent)
92+
{
93+
TRACE_CPUPROFILER_EVENT_SCOPE(UTolgeeLocalizationInjectorSubsystem::ExtractTranslationsFromPO)
94+
95+
TArray<FTolgeeTranslationData> Result;
96+
97+
#if WITH_LOCALIZATION_MODULE
98+
FPortableObjectFormatDOM PortableObject;
99+
PortableObject.FromString(PoContent);
100+
101+
for (auto EntryPairIter = PortableObject.GetEntriesIterator(); EntryPairIter; ++EntryPairIter)
102+
{
103+
auto POEntry = EntryPairIter->Value;
104+
if (POEntry->MsgId.IsEmpty() || POEntry->MsgStr.Num() == 0 || POEntry->MsgStr[0].IsEmpty())
105+
{
106+
// We ignore the header entry or entries with no translation.
107+
continue;
108+
}
109+
110+
FTolgeeTranslationData TranslationData;
111+
112+
constexpr ELocalizedTextCollapseMode InTextCollapseMode = ELocalizedTextCollapseMode::IdenticalTextIdAndSource;
113+
constexpr EPortableObjectFormat InPOFormat = EPortableObjectFormat::Crowdin;
114+
115+
PortableObjectPipeline::ParseBasicPOFileEntry(*POEntry, TranslationData.ParsedNamespace, TranslationData.ParsedKey, TranslationData.SourceText, TranslationData.Translation, InTextCollapseMode, InPOFormat);
116+
117+
Result.Add(TranslationData);
118+
}
119+
#else
120+
UE_LOG(LogTolgee, Error, TEXT("Localization module is not available. Cannot extract translations from PO content."));
121+
#endif
122+
123+
return Result;
124+
}

0 commit comments

Comments
 (0)