Architecture Overview

The plugin has one guiding rule: CLIFF is a step of the official localization pipeline, not a replacement for it. It therefore never patches the engine, never writes .locres binaries itself and never builds its own Dashboard window — it plugs into extension points the engine already exposes.

Goals and constraints

GoalHow it is achieved
No engine modificationsOnly public Core / Developer / Editor types; zero engine diff
No bypassing official dataWrites go through the official FLocTextHelper and FTextLocalizationResourceGenerator
Native to the official workflowRegisters an official LocalizationService provider; buttons are injected into the official Dashboard toolbar
Headless capableImport, export and AI translation are all UGatherTextCommandletBase subclasses drivable by -run=GatherText
No runtime dependenciesThe Runtime module has no editor dependencies, no Python and no HTTP

Module layout

Plugins/CliffLocalizationSuite/
├── CliffLocalizationSuite.uplugin
├── Content/Localization/            # bundled translations (7 targets × 15 languages)
└── Source/
    ├── CliffLocalizationSuite/       # Runtime: data model and parsing
    │   ├── Public/Cliff/CliffDocument.h
    │   ├── Public/Cliff/CliffSemanticMapping.h
    │   ├── Public/Cliff/CliffSerializer.h
    │   ├── Public/Settings/CliffLocalizationSuiteSettings.h
    │   └── Private/…                # parser, validator, settings, tests
    └── CliffLocalizationSuiteEditor/ # Editor: pipeline integration
        ├── Public/Cliff/CliffImporter.h
        ├── Public/Cliff/CliffExporter.h
        ├── Public/AI/CliffAITranslator.h
        ├── Public/Commandlets/CliffGatherCommandlets.h
        └── Private/…                # toolbox, provider, progress window, AI client, tests
ModuleTypeLoading phaseResponsibility
CliffLocalizationSuiteRuntimeDefaultCLIFF data model, parser, validator, serializer, semantic mapping, UDeveloperSettings
CliffLocalizationSuiteEditorEditorDefaultImporter, exporter, AI translation, Dashboard integration, GatherText commandlets, console commands

Dependencies

// Runtime: pure data and algorithms, safe for Game targets
PublicDependencyModuleNames: Core, CoreUObject, Engine, DeveloperSettings

// Editor: official pipeline and editor UI
PublicDependencyModuleNames:  CliffLocalizationSuite, Core, CoreUObject, Engine,
                              LocalizationService, Slate, SlateCore
PrivateDependencyModuleNames: ApplicationCore, DesktopPlatform, EngineSettings, HTTP, InputCore,
                              Json, Localization, LocalizationCommandletExecution, UnrealEd

NOTE

The Runtime module does not depend on UnrealEd, AssetTools or Slate, so Game and Shipping targets package cleanly. The Editor module is Type: Editor and never ships at runtime.

Official integration points

The UE 5.8 localization pipeline is: editor action → generated ini → GatherText subprocess runs steps → manifest / archive → FTextLocalizationResourceGenerator produces .locmeta / .locresFTextLocalizationManager lookups.

There is no registration API for third-party file formats (PO and CSV are hard-coded commandlet behaviour inside the ini the Dashboard generates), so the plugin uses four official extension points:

#Extension pointOfficial typeHow the plugin uses it
1GatherText step discoveryGatherTextStep{N} + CommandletClass in the ini, reflected by UGatherTextCommandletCliffImport / CliffExport / CliffAITranslate become ordinary pipeline steps
2Localization Service ProviderThe ILocalizationServiceProvider modular feature (registered as "LocalizationService")Injects the CLIFF buttons into the official Dashboard target and target-set toolbars
3Official compilationFTextLocalizationResourceGenerator::GenerateLocMeta/GenerateLocRes.locmeta / .locres are produced by the engine, never by the plugin
4Official refreshFTextLocalizationManager::UpdateFromLocalizationResource, LocalizationDelegates::OnLocalizationTargetDataUpdatedImport refreshes live text and the Dashboard cache immediately

Key classes

Class / structModulePurpose
FCliffDocumentRuntimeParse entry point: Parse / ParseAndValidate / Validate / GetCanonicalId
FCliffHeader / FCliffGroup / FCliffEntryRuntimeThe CLIFF data model
FCliffValidationIssue / ECliffValidationCategoryRuntimeSeven issue categories; IsError() excludes extension and warning
FCliffSemanticMappingRuntimeclan ↔ UE Namespace and Namespace → type / emotion tables
FCliffSerializerRuntimeCanonical CLIFF 1.0 serialization (UTF-8 without BOM, LF)
UCliffLocalizationSuiteSettingsRuntimeProject Settings (Identity / Semantic Mapping / Workflow / AI Translation)
FCliffImporterEditor.cliff → manifest / archive → compile → live refresh
FCliffExporterEditormanifest + archive (or .locres) → grouped .cliff plus a key sidecar
FCliffEditorToolboxEditorDashboard buttons, file dialogs, GatherText config generation, task assembly
FCliffLocalizationServiceProviderEditorThe official provider implementation (toolbar injection)
FCliffAITranslatorEditorDeepSeek orchestration, self-translation detection, connection test
UCliffImportCommandlet / UCliffExportCommandlet / UCliffAITranslateCommandletEditorThe three official GatherText steps
FCliffCommandletExecutorUIEditorThe official-style Slate progress window

Trade-offs

Why a custom commandlet plus provider injection instead of a bespoke window

The official Dashboard offers no way to add a new format button, but it does expose the ILocalizationServiceProvider toolbar hook and GatherText step discovery. Going through them keeps the logs identical to the official ones (LogGatherTextCommandlet sectioned output), keeps subprocess behaviour identical, and lets CI reuse the same ini configuration. The cost is that button behaviour is bound to the Dashboard’s target/target-set context.

Why a sidecar is mandatory

.locres stores only a SourceStringHash — no source text and no metadata — while UE keys are frequently the source text itself (Chinese, |, capitals and / are all legal), which cannot be a CLIFF name. The exporter therefore generates stable IDs and records “original UE Namespace / Key ↔ canonical ID” in .cliffmap.json, which the importer replays for a lossless round trip.

Why status is degraded on export

The official review model is binary — “has a translation whose source matches ⇒ reviewed” — with no reviewed / final distinction and no emotion. The plugin therefore stores the CLIFF four-state status and emotion in the archive’s KeyMetadataObj (cliff.status, cliff.emotion) and in the sidecar; where that information is missing, status degrades to the official semantics (no translation → initial, translation present → treated as reviewed).

Why ICU is passed through verbatim

UE’s FText::Format only understands plural / ordinal / gender / hpp and does not evaluate ICU MessageFormat at runtime. The plugin therefore stores and returns ICU strings unchanged and compiles .locres with EGenerateLocResFlags::None to disable format validation; CLIFF-side brace balancing still runs during parsing.

Bundled localization data

The plugin’s own UI strings ship with it. The .uplugin declares seven targets:

TargetLoading policy
CliffLocalizationSuiteRuntimeAlways
CliffLocalizationSuiteEditorEditor
CliffLocalizationSuiteEditorTutorialsNever
CliffLocalizationSuitePropertyNamesPropertyNames
CliffLocalizationSuiteToolTipsToolTips
CliffLocalizationSuiteKeywordsEditor
CliffLocalizationSuiteCategoryEditor

Each target covers native en plus 15 languages (zh-Hans, en, ja, ko, es, pt, ar, pl, de, ru, fr, pt-BR, tr, es-419, it) under Content/Localization/. This is exactly why the AI pipeline compiles the native culture too — otherwise the editor UI’s own language .locres would never be rebuilt.

Next