AI Translation and Commandlets
FCliffAITranslator
struct CLIFFLOCALIZATIONSUITEEDITOR_API FCliffAITranslationStats
{
int32 CandidateCount = 0; // 进入请求的候选条目累计(含重跑轮次)
int32 TranslatedCount = 0; // 成功写回并置 translated
int32 SkippedCount = 0; // 已有 target 且非重译候选
int32 FailedCount = 0; // 无回包/空译文/占位符不符/自翻译/修复后仍非法/写盘失败
int32 RepairCount = 0; // 成功修复次数
int32 BatchCount = 0; // 批次数
TArray<FString> FailedFiles;
};
class CLIFFLOCALIZATIONSUITEEDITOR_API FCliffAITranslator
{
public:
static bool TranslateCliffDirectory(const FString& InCliffDirectory,
const TArray<FString>& InCultures,
const UCliffLocalizationSuiteSettings* InSettings,
FCliffAITranslationStats& OutStats,
FText& OutError,
int32 InRetryDepth = 0);
static bool TestConnection(const UCliffLocalizationSuiteSettings* InSettings,
FString& OutMessage, FText& OutError);
static bool IsSelfTranslation(const FString& InSource, const FString& InTarget, const FString& InTargetLanguage);
static bool IsRetranslationCandidate(const FString& InSource, const FString& InTarget, const FString& InStatus,
const FString& InTargetLanguage, const bool bEnabled);
};
Example
const UCliffLocalizationSuiteSettings* Settings = GetDefault<UCliffLocalizationSuiteSettings>();
// 1) 连接自检
FString Message; FText Error;
if (!FCliffAITranslator::TestConnection(Settings, Message, Error))
{
UE_LOG(LogTemp, Error, TEXT("DeepSeek: %s"), *Error.ToString());
}
// 2) 翻译一个目录下的全部 .cliff(按 culture 筛选)
FCliffAITranslationStats Stats;
if (FCliffAITranslator::TranslateCliffDirectory(
FPaths::ProjectSavedDir() / TEXT("CliffAITranslate/Game"),
{ TEXT("ja"), TEXT("ko") }, Settings, Stats, Error))
{
UE_LOG(LogTemp, Log, TEXT("translated %d / candidates %d, failed %d, repairs %d"),
Stats.TranslatedCount, Stats.CandidateCount, Stats.FailedCount, Stats.RepairCount);
}
// 3) 判定辅助函数
const bool bSelf = FCliffAITranslator::IsSelfTranslation(
TEXT("DeepSeek response has no message content."), TEXT("DeepSeek response has no message content."), TEXT("zh-Hans")); // true
const bool bCandidate = FCliffAITranslator::IsRetranslationCandidate(
TEXT("Play"), TEXT("Play"), TEXT("translated"), TEXT("zh-Hans"), /*bEnabled=*/true); // true
Behaviour contract
| Item | Rule |
|---|
| Candidate selection | Has a source and no target; or status == "translated" and judged to be a self-translation and the switch is on |
| Never touched | reviewed / final |
| Empty translation | Source already in the target language → fill in the source text locally; otherwise write-back is refused |
| Self-translation | Write-back is refused and counted as a failure |
| Placeholders | The counts must be equal and every {…} in the source must appear in the translation, otherwise write-back is refused |
| Retry | Up to MaxRetries retries per batch (a 0.5s incremental interval); if failures remain after a full round it runs again (at most MaxRetries rounds) |
| Repair | When ParseAndValidate fails after write-back and bAutoRepairCliff is on → up to MaxRepairAttempts rounds; if it still fails, nothing is written to disk and the file is recorded in FailedFiles |
| Overall failure | Only when “candidates > 0 and translations == 0”; partial success only produces a warning |
| Concurrency | Batches are issued concurrently in chunks of MaxConcurrentCultures and then awaited together |
| Key resolution | The DEEPSEEK_API_KEY environment variable wins, otherwise the settings field; the key never appears in logs or configuration |
| Script detection | Four families — Latin / CJK / Arabic / Cyrillic — judged by the target language prefix (zh/ja/ko, ar/fa/he/ur, ru/uk/bg/sr, everything else treated as Latin) to decide whether the text “is already in the target language” |
GatherText commandlets
UCLASS()
class CLIFFLOCALIZATIONSUITEEDITOR_API UCliffImportCommandlet : public UGatherTextCommandletBase
{
virtual int32 Main(const FString& Params) override;
virtual EGatherTextCommandletPhase GetPhase() const override { return EGatherTextCommandletPhase::Import; }
};
UCLASS()
class CLIFFLOCALIZATIONSUITEEDITOR_API UCliffExportCommandlet : public UGatherTextCommandletBase
{
virtual int32 Main(const FString& Params) override;
virtual EGatherTextCommandletPhase GetPhase() const override { return EGatherTextCommandletPhase::Export; }
};
UCLASS()
class CLIFFLOCALIZATIONSUITEEDITOR_API UCliffAITranslateCommandlet : public UGatherTextCommandletBase
{
virtual int32 Main(const FString& Params) override;
virtual EGatherTextCommandletPhase GetPhase() const override { return EGatherTextCommandletPhase::Import; }
};
| Commandlet | CommandletClass | Required config keys | Returns |
|---|
UCliffImportCommandlet | CliffImport | SourcePath / ManifestName / ArchiveName / NativeCulture, plus CliffFilePath[] or CliffDirectory | 0 on success, -1 on failure |
UCliffExportCommandlet | CliffExport | SourcePath / DestinationPath / ManifestName / ArchiveName / NativeCulture / CliffNamespace | 0 / -1 |
UCliffAITranslateCommandlet | CliffAITranslate | SourcePath / DestinationPath / CulturesToGenerate[] | 0 / -1 |
They are invoked by the official executor with -Config="<ini>" -Section="GatherTextStep{N}"; when either parameter is missing they report No config or section specified. and return -1. The complete key list is in commands and commandlets.
Toolbox entry points (Editor-only)
namespace FCliffEditorToolbox
{
bool BuildImportSettings(const ULocalizationTarget*, FCliffImportSettings&, FText&);
bool BuildExportSettings(const ULocalizationTarget*, FCliffExportSettings&, FText&);
bool ImportCliffFile(const ULocalizationTarget*, const FString& InCliffFilePath, FCliffImportStats&, TArray<FCliffValidationIssue>&, FText&);
bool ImportCliffFileOfficial(const ULocalizationTarget*, const FString& InCliffFilePath, FText&);
bool ImportCliffFilesOfficial(const ULocalizationTarget*, const TArray<FString>&, FText&);
bool ExportTargetToDirectory(const ULocalizationTarget*, const FString& InOutputDirectory, TArray<FCliffExportDocument>&, FCliffExportStats&, FText&);
bool ExportTargetToDirectoryOfficial(const ULocalizationTarget*, const FString& InOutputDirectory, FText&);
bool AITranslateTargetsOfficial(const ULocalizationTarget*, const TArray<FString>& InCultures, const FString& InSourceLanguage, FText&);
bool AITranslateTargetsOfficial(const TArray<ULocalizationTarget*>&, const TArray<FString>&, const FString&, FText&);
bool ExportTargetsToDirectoryOfficial(const TArray<ULocalizationTarget*>&, const FString&, FText&);
bool ImportCliffDirectoryOfficialForTargets(const TArray<ULocalizationTarget*>&, const FString&, FText&);
void ImportFromDialog(const TWeakObjectPtr<ULocalizationTarget>&);
void ImportFromDialog(const TWeakObjectPtr<ULocalizationTargetSet>&);
void ExportToDialog(const TWeakObjectPtr<ULocalizationTarget>&);
void ExportToDialog(const TWeakObjectPtr<ULocalizationTargetSet>&);
void AITranslateFromDialog(const TWeakObjectPtr<ULocalizationTarget>&);
void AITranslateFromDialog(const TWeakObjectPtr<ULocalizationTargetSet>&);
void NotifyTargetSetAction(const FText& InActionName);
}
| Naming | Description |
|---|
The …Official suffix | Goes through the official GatherText pipeline (generating config, starting the subprocess, running reports and refreshing the Dashboard); these are what the GUI and CLI use |
No suffix (ImportCliffFile / ExportTargetToDirectory) | Calls the importer / exporter directly without starting a subprocess, which suits tools and tests |
*FromDialog | The actual entry point behind the Dashboard buttons: it pops the directory or Culture selection window and then dispatches to the official entry points above |
CliffLocalizationServiceProvider
class FCliffLocalizationServiceProvider : public ILocalizationServiceProvider
{
const FName& GetName() const override; // "CliffLocalizationSuite"
const FText GetDisplayName() const override; // "CLIFF Localization Suite"
FText GetStatusText() const override; // "CLIFF import/export is ready."
bool IsEnabled() const override; // true
bool IsAvailable() const override; // true
void CustomizeTargetToolbar(TSharedRef<FExtender>&, TWeakObjectPtr<ULocalizationTarget>) const override;
void CustomizeTargetSetToolbar(TSharedRef<FExtender>&, TWeakObjectPtr<ULocalizationTargetSet>) const override;
};
At module startup it registers itself as the "LocalizationService" modular feature and sets itself as the current Provider (otherwise the Dashboard snapshots the default Provider and the buttons never appear).