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

ItemRule
Candidate selectionHas a source and no target; or status == "translated" and judged to be a self-translation and the switch is on
Never touchedreviewed / final
Empty translationSource already in the target language → fill in the source text locally; otherwise write-back is refused
Self-translationWrite-back is refused and counted as a failure
PlaceholdersThe counts must be equal and every {…} in the source must appear in the translation, otherwise write-back is refused
RetryUp to MaxRetries retries per batch (a 0.5s incremental interval); if failures remain after a full round it runs again (at most MaxRetries rounds)
RepairWhen 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 failureOnly when “candidates > 0 and translations == 0”; partial success only produces a warning
ConcurrencyBatches are issued concurrently in chunks of MaxConcurrentCultures and then awaited together
Key resolutionThe DEEPSEEK_API_KEY environment variable wins, otherwise the settings field; the key never appears in logs or configuration
Script detectionFour 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; }
};
CommandletCommandletClassRequired config keysReturns
UCliffImportCommandletCliffImportSourcePath / ManifestName / ArchiveName / NativeCulture, plus CliffFilePath[] or CliffDirectory0 on success, -1 on failure
UCliffExportCommandletCliffExportSourcePath / DestinationPath / ManifestName / ArchiveName / NativeCulture / CliffNamespace0 / -1
UCliffAITranslateCommandletCliffAITranslateSourcePath / 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);
}
NamingDescription
The …Official suffixGoes 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
*FromDialogThe 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).