Domain Services
The rules, without the windows
These classes live in Decnet.LocalizeShip.Core.Services. They have no editor dependencies and can be called from tests, scripts or your own tooling.
RevisionTracker
public RevisionTracker(NormalizationPolicy policy = null)
SourceRevision ProcessSourceEntry(
ReleaseWorkspace workspace,
StableEntryId entryId,
string rawText,
string speaker,
string context,
string fixture = null)
TranslationRevisionHash ComputeTranslationHash(string translationText)
Behaviour:
- Builds a new
SourceRevisionwith the tracker’s policy and optionalSceneOrFixture. - If no current revision: adds it and calls
CheckSourceChangeon all approvals for the entry. - If the hash differs: removes the old revision, adds the new one, stales approved records, and rechecks voice entries using the approval’s
ApprovedTranslationHash(or empty). - If the hash is identical: returns the new object but does not replace the stored revision or touch states.
Note: it compares hashes, so a timestamp-only change does nothing.
RevisionTrackerowns its ownNormalizationPolicy(default or constructor-supplied). Passworkspace.Normalizationif you want it to match the workspace. Constructingnew RevisionTracker()while the workspace uses a custom policy can produce hashes the rest of the workspace does not expect.
ImpactEngine
public ImpactReport ComputeImpact(ReleaseWorkspace workspace, StableEntryId entryId)
ImpactReport contains ChangedEntryId, TriggerSourceHash and a list of ImpactedArtifact:
public class ImpactedArtifact
{
public ImpactCategory Category;
public StableEntryId EntryId;
public LocaleId Locale;
public string Title;
public string Reason;
public string Snippet;
public string TargetAssetPath;
public string TargetId;
}
Categories emitted:
| Category | Condition |
|---|---|
ChangedSource | A source revision exists for the entry |
StaleTranslation | Approval is Stale, Missing, or absent |
VoiceReRecord | Voice entry is stale or missing |
SubtitleReview | Cue’s BoundSourceHash differs from the current source hash |
VisualRecheck | A capture finding references the entry, or the tile fixture ID equals SceneOrFixture |
It does not check review states other than stale/missing, does not verify voice durations, and does not know about provider assets. Capture matching by fixture ID is string equality, case-insensitive.
MergePlanner
public MergePlan CreatePlan(
ReleaseWorkspace workspace,
HandoffManifest baseManifest,
List<HandoffEntry> incomingEntries,
LocaleId targetLocale)
public static string ProtectFormula(string text)
public static string UnprotectFormula(string text)
Classification order (first match wins):
- duplicate incoming ID →
DuplicateIdError; - no source revision and no approval →
UnknownIdError; - blank incoming text →
BlankIncomingValue; - base source hash ≠ current source hash →
StaleSourceConflict; - current translation differs from base translation and from incoming →
ConcurrentLocaleEdit; - current equals incoming →
Unchanged; - otherwise →
SafeIncomingChange, resolved to incoming, resolutionTakeIncoming.
Rows for other locales are skipped. The base lookup only includes entries whose TargetLocale == targetLocale.
ProtectFormula prefixes =, +, -, @, tab or CR with '. UnprotectFormula removes it only when the next character is one of those.
ApplyCoordinator
ImportJournal ApplyPlan(ReleaseWorkspace workspace, MergePlan plan, string reviewerName)
void Rollback(ReleaseWorkspace workspace, ImportJournal journal, bool force = false)
ApplyPlan:
- throws if
plan.HasUnresolvedConflicts; - skips
IsBlockingErrorandUnchangedrows; - computes the value as
ResolvedValue, else incoming forTakeIncoming, else current; - records a backup for each applied row;
KeepCurrentrecords a backup then leaves text and state untouched;- applies the text, then
ApplyReview(NeedsReview, currentSourceHash, default, reviewerName, "Translator Import", ...).
Rollback:
- refuses if any row changed after apply, unless
force; - restores text, state and
ApprovedSourceHash; - does not restore
ApprovedTranslationHash; - does not remove approval events;
- sets
IsRolledBack.
Neither method calls EditorUtility.SetDirty or AssetDatabase.SaveAssets; the window does that after apply.
TextRuleEngine
List<TextRuleViolation> ValidatePlaceholders(
SourceRevision source, string translationText, LocaleId locale)
List<GlossaryViolation> CheckGlossary(
string text, StableEntryId entryId, LocaleId locale, List<GlossaryTerm> terms)
static string GeneratePseudoText(
string originalText, float expansionFactor = 1.35f, bool rtlSimulation = false)
ValidatePlaceholders checks missing/unexpected placeholders, brace balance and rich-text tag pairing. Source and translation are compared by tag string; plural arguments are reduced to the token before the first comma.
CheckGlossary searches forbidden variants only.
GeneratePseudoText accents vowels, pads for expansion, wraps in [!! ... !!] and optionally reverses with bidi marks.
VisualRuleEngine
List<VisualFinding> EvaluateElement(
StableEntryId entryId,
LocaleId locale,
string objectName,
Rect elementRect,
Rect containerRect,
Rect screenSafeArea,
bool isInteractive = false,
string renderedText = null,
List<char> missingGlyphs = null,
List<FindingSuppression> suppressions = null)
Emits:
| Finding | Severity | Rule |
|---|---|---|
TextOverflow | Error | element width/height exceeds container by more than 1 px |
SafeAreaViolation | Warning | element rect outside the safe-area rect |
TouchTargetTooSmall | Warning | interactive element under 44×44 |
MissingGlyph | Error | any missing glyph supplied |
Suppression is applied by exact fingerprint match.
The safe-area comparison uses the rects as supplied.
CaptureRunnerpassesScreen.safeArea(screen space) and local RectTransform rects, which are not the same coordinate space. Confirm results on your canvas setup rather than assuming geometric precision.
AdvisoryProvider
DraftProposal ValidateSuggestion(
SourceRevision source, string rawSuggestionJson, LocaleId targetLocale)
Parses { id, suggestion, rationale }, requires the ID to equal the source entry ID, and runs placeholder validation. Any Error/Fatal makes the proposal invalid. It performs no network I/O and never approves text.
HandoffCsvSerializer
static string SerializeCsv(HandoffManifest manifest)
static HandoffManifest DeserializeCsv(string csvText, LocaleId targetLocale)
static List<List<string>> ParseCsvRows(string csvText)
The parser handles quoted fields, doubled quotes, CRLF and LF. Serialization protects formulas; deserialization unprotects. The CSV loses constraints and base translation text by design.