Model/XlfDocument.ps1

class XlfDocument {

    hidden [System.Xml.XmlNode] $root;
    hidden [System.Xml.XmlNode] $cachedImportParentNode;
    hidden [System.Xml.XmlNode[]] $cachedTranslationUnitNodes;

    hidden $idUnitMap;
    hidden $xliffGeneratorNoteSourceUnitMap;
    hidden $xliffGeneratorNoteDeveloperNoteUnitMap;
    hidden $xliffGeneratorNoteUnitMap;
    hidden $xliffGeneratorNote;
    hidden $sourceDeveloperNoteUnitMap;
    hidden $sourceUnitMap;

    [string] $developerNoteDesignation;
    [string] $xliffGeneratorNoteDesignation;
    [boolean] $preserveTargetAttributes;
    [boolean] $preserveTargetAttributesOrder;
    [string] $parseFromDeveloperNoteSeparator;
    [string] $missingTranslation;

    [boolean] Valid() {
        $hasRoot = $null -ne $this.root;
        $hasVersion = $null -ne $this.Version();
        $hasSourceLanguage = $null -ne $this.GetSourceLanguage();
        return $hasRoot -and $hasVersion -and $hasSourceLanguage;
    }

    [string] Version() {
        return $this.root.'version';
    }

    [string] GetSourceLanguage() {
        switch ($this.Version()) {
            "1.2" {
                [System.Xml.XmlNode] $fileNode = [XlfDocument]::GetNode('file', $this.root);
                if ($fileNode) {
                    return $fileNode.'source-language';
                }
            }
        }
        return $null;
    }

    [void] SetSourceLanguage([string] $lng) {
        if (-not $lng) {
            return;
        }

        switch ($this.Version()) {
            "1.2" {
                [System.Xml.XmlNode] $fileNode = [XlfDocument]::GetNode('file', $this.root);
                if ($fileNode) {
                    $fileNode.'source-language' = $lng
                }
                break;
            }
        }
    }

    [string] GetTargetLanguage() {
        switch ($this.Version()) {
            "1.2" {
                [System.Xml.XmlNode] $fileNode = [XlfDocument]::GetNode('file', $this.root);
                if ($fileNode) {
                    return $fileNode.'target-language';
                }
            }
        }
        return $null;
    }

    [void] SetTargetLanguage([string] $lng) {
        if (-not $lng) {
            return;
        }

        switch ($this.Version()) {
            "1.2" {
                [System.Xml.XmlNode] $fileNode = [XlfDocument]::GetNode('file', $this.root);
                if ($fileNode) {
                    $fileNode.'target-language' = $lng;
                }
            }
        }
    }

    [void] CreateUnitMaps([bool] $findByXliffGeneratorNoteAndSource, [bool] $findByXliffGeneratorAndDeveloperNote, [bool] $findByXliffGeneratorNote, [bool] $findBySourceAndDeveloperNote, [bool] $findBySource) {
        [bool] $findByXliffGenNotesIsEnabled = $findByXliffGeneratorNoteAndSource -or $findByXliffGeneratorAndDeveloperNote -or $findByXliffGeneratorNote;
        [bool] $findByIsEnabled = $findByXliffGenNotesIsEnabled -or $findBySourceAndDeveloperNote -or $findBySource;

        $this.idUnitMap = @{}
        $this.xliffGeneratorNoteSourceUnitMap = @{};
        $this.xliffGeneratorNoteDeveloperNoteUnitMap = @{};
        $this.xliffGeneratorNoteUnitMap = @{};
        $this.sourceDeveloperNoteUnitMap = @{};
        $this.sourceUnitMap = @{};

        $this.TranslationUnitNodes() | ForEach-Object {
            $unit = $_;
            if (-not $this.idUnitMap.Contains($unit.id)) {
                $this.idUnitMap.Add($unit.id, $unit);
            }

            if ($findByIsEnabled) {
                [string] $developerNote = $this.GetUnitDeveloperNote($unit);
                [string] $sourceText = $this.GetUnitSourceText($unit);

                if ($findByXliffGenNotesIsEnabled) {
                    [string] $xliffGeneratorNote = $this.GetUnitXliffGeneratorNote($unit);

                    if ($findByXliffGeneratorNoteAndSource) {
                        $key = @($xliffGeneratorNote, $sourceText);
                        if (-not $this.xliffGeneratorNoteSourceUnitMap.ContainsKey($key)) {
                            $this.xliffGeneratorNoteSourceUnitMap.Add($key, $unit);
                        }
                    }
                    if ($findByXliffGeneratorAndDeveloperNote) {
                        $key = @($xliffGeneratorNote, $developerNote);
                        if (-not $this.xliffGeneratorNoteDeveloperNoteUnitMap.ContainsKey($key)) {
                            $this.xliffGeneratorNoteDeveloperNoteUnitMap.Add($key, $unit);
                        }
                    }
                    if ($findByXliffGeneratorNote) {
                        $key = $xliffGeneratorNote;
                        if (-not $this.xliffGeneratorNoteUnitMap.ContainsKey($key)) {
                            $this.xliffGeneratorNoteUnitMap.Add($key, $unit);
                        }
                    }
                }

                if ($findBySourceAndDeveloperNote) {
                    $key = @($sourceText, $developerNote);
                    if (-not ($this.sourceDeveloperNoteUnitMap.ContainsKey($key))) {
                        $translation = $this.GetUnitTranslation($unit);
                        if ($translation) {
                            $this.sourceDeveloperNoteUnitMap.Add($key, $unit);
                        }
                    }
                }

                if ($findBySource -and (-not ($this.sourceUnitMap.ContainsKey($sourceText)))) {
                    $translation = $this.GetUnitTranslation($unit);
                    if ($translation) {
                        $this.sourceUnitMap.Add($sourceText, $unit);
                    }
                }
            }
        }
    }

    [System.Xml.XmlNode] FindTranslationUnit([string] $transUnitId) {
        if ($this.idUnitMap) {
            if ($this.idUnitMap.ContainsKey($transUnitId)) {
                return $this.idUnitMap[$transUnitId];
            }
            return $null;
        } else {
            return $this.TranslationUnitNodes() | Where-Object { $_.'id' -eq $transUnitId } | Select-Object -First 1;
        }
    }

    [System.Xml.XmlNode] FindTranslationUnitByXliffGeneratorNoteAndSourceText([string] $xliffGenNote, [string] $sourceText) {
        if ($this.xliffGeneratorNoteSourceUnitMap) {
            $key = @($xliffGenNote, $sourceText);
            if ($this.xliffGeneratorNoteSourceUnitMap.ContainsKey($key)) {
                return $this.xliffGeneratorNoteSourceUnitMap[$key];
            }
            return $null;
        } else {
            return $this.TranslationUnitNodes() | Where-Object { ($this.GetUnitXliffGeneratorNote($_) -eq $xliffGenNote) -and ($this.GetUnitSourceText($_) -eq $sourceText) } | Select-Object -First 1;
        }
    }

    [System.Xml.XmlNode] FindTranslationUnitByXliffGeneratorNoteAndDeveloperNote([string] $xliffGenNote, [string] $devNote) {
        if ($this.xliffGeneratorNoteDeveloperNoteUnitMap) {
            $key = @($xliffGenNote, $devNote);
            if ($this.xliffGeneratorNoteDeveloperNoteUnitMap.ContainsKey($key)) {
                return $this.xliffGeneratorNoteDeveloperNoteUnitMap[$key];
            }
            return $null;
        } else {
            return $this.TranslationUnitNodes() | Where-Object { ($this.GetUnitXliffGeneratorNote($_) -eq $xliffGenNote) -and ($this.GetUnitDeveloperNote($_) -eq $devNote) } | Select-Object -First 1;
        }
    }

    [System.Xml.XmlNode] FindTranslationUnitByXliffGeneratorNote([string] $xliffGenNote) {
        if ($this.xliffGeneratorNoteUnitMap) {
            $key = $xliffGenNote;
            if ($this.xliffGeneratorNoteUnitMap.ContainsKey($key)) {
                return $this.xliffGeneratorNoteUnitMap[$key];
            }
            return $null;
        } else {
            return $this.TranslationUnitNodes() | Where-Object { ($this.GetUnitXliffGeneratorNote($_) -eq $xliffGenNote) } | Select-Object -First 1;
        }
    }

    [System.Xml.XmlNode] FindTranslationUnitBySourceTextAndDeveloperNote([string] $sourceText, [string] $devNote) {
        if ($this.sourceDeveloperNoteUnitMap) {
            $key = @($sourceText, $devNote);
            if ($this.sourceDeveloperNoteUnitMap.ContainsKey($key)) {
                return $this.sourceDeveloperNoteUnitMap[$key];
            }
            return $null;
        } else {
            return $this.TranslationUnitNodes() | Where-Object { ($this.GetUnitDeveloperNote($_) -eq $devNote) -and ($this.GetUnitSourceText($_) -eq $sourceText) } | Select-Object -First 1;
        }
    }

    [System.Xml.XmlNode] FindTranslationUnitBySourceText([string] $sourceText) {
        if ($this.sourceUnitMap) {
            $key = $sourceText;
            if ($this.sourceUnitMap.ContainsKey($key)) {
                return $this.sourceUnitMap[$key];
            }
            return $null;
        } else {
            return $this.TranslationUnitNodes() | Where-Object { ($this.GetUnitSourceText($_) -eq $sourceText) } | Select-Object -First 1;
        }
    }

    [void] ImportUnit([System.Xml.XmlNode] $unit) {
        $newUnit = $this.root.OwnerDocument.ImportNode($unit, $true);

        [System.Xml.XmlNode] $parentNode = $this.cachedImportParentNode;
        if (-not $this.cachedImportParentNode) {
            switch ($this.Version()) {
                "1.2" {
                    # Parent will be the first 'group' or 'body' node.
                    $parentNode = [XlfDocument]::GetNode('group', $this.root);
                    if (-not $parentNode) {
                        $parentNode = [XlfDocument]::GetNode('body', $this.root);
                    }
                    break;
                }
            }

            if (-not $parentNode) {
                return;
            }

            $this.cachedImportParentNode = $parentNode;
        }

        $parentNode.AppendChild($newUnit);
    }

    [void] MergeUnit([System.Xml.XmlNode] $sourceUnit, [System.Xml.XmlNode] $targetUnit, [string] $translation) {
        [System.Xml.XmlNode] $targetNode = $null;

        [System.Xml.XmlElement] $sourceUnitAsElement = $sourceUnit;
        if ($targetUnit) {
            if ($this.preserveTargetAttributes) {
                # Use the target's attribute values
                if ($this.preserveTargetAttributesOrder) {
                    $sourceAttributes = @{};
                    foreach ($attr in $sourceUnit.Attributes) {
                        $sourceAttributes[$attr.Name] = $attr.Value;
                    }

                    # First take the target's attributes
                    $sourceUnitAsElement.Attributes.RemoveAll();
                    foreach ($attr in $targetUnit.Attributes) {
                        $newAttr = $this.root.OwnerDocument.ImportNode($attr, $true);
                        $sourceUnitAsElement.SetAttributeNode($newAttr);
                    }

                    # Use the id from the sourceUnit
                    if ($sourceAttributes.ContainsKey('id')) {
                        $newAttr = $this.root.OwnerDocument.CreateAttribute('id');
                        $newAttr.Value = $sourceAttributes['id'];
                        $sourceUnitAsElement.SetAttributeNode($newAttr);
                    }

                    # Add the extra attributes from the sourceUnit
                    foreach ($attr in $sourceAttributes.GetEnumerator()) {
                        if (-not $sourceUnitAsElement.Attributes[$attr.Name]) {
                            $sourceUnitAsElement.SetAttribute($attr.Name, $attr.Value);
                        }
                    }
                } else {
                    foreach ($attr in $targetUnit.Attributes) {
                        if ($attr.Name -ne 'id') {
                            $newAttr = $this.root.OwnerDocument.ImportNode($attr, $true);
                            $sourceUnitAsElement.SetAttributeNode($newAttr);
                        }
                    }
                }
            } else {
                # Use the source's attribute values for the attributes in common, and extend these with any extra attributes from the target.
                foreach ($attr in $targetUnit.Attributes) {
                    if (-not $sourceUnitAsElement.Attributes[$attr.Name]) {
                        $newAttr = $this.root.OwnerDocument.ImportNode($attr, $true);
                        $sourceUnitAsElement.SetAttributeNode($newAttr);
                    }
                }
            }

            $targetUnitTargetNode = [XlfDocument]::GetNode('target', $targetUnit);
            if ($targetUnitTargetNode) {
                $targetNode = $this.root.OwnerDocument.ImportNode($targetUnitTargetNode, $true);
            }
        }

        [boolean] $needsTranslation = $this.GetUnitNeedsTranslation($sourceUnit);
        if ($needsTranslation -and (-not $targetNode)) {
            [XlfTranslationState] $newTranslationState = [XlfTranslationState]::Translated;
            if (-not $translation) {
                $translation = $this.missingTranslation;
                $newTranslationState = [XlfTranslationState]::MissingTranslation;
            }
            $targetNode = $this.CreateTargetNode($sourceUnit, $translation, $newTranslationState);
        } elseif ((-not $needsTranslation) -and $targetNode) {
            $this.DeleteTargetNode($sourceUnit);
        }

        if ($needsTranslation -and $targetNode) {
            if ($translation) {
                $targetNode.InnerText = $translation;
                if ($this.Version() -eq "1.2") {
                    $this.UpdateStateAttributes($targetNode, [XlfTranslationState]::Translated);
                }
            }

            $this.AppendTargetNode($sourceUnit, $targetNode);
        }
    }

    [void] UpdateStateAttributes([System.Xml.XmlElement] $stateNode, [XlfTranslationState] $translationState) {
        switch ($this.Version()) {
            "1.2" {
                switch ($translationState) {
                    ([XlfTranslationState]::MissingTranslation) {
                        $stateNode.SetAttribute('state', 'needs-translation');
                        break;
                    }
                    ([XlfTranslationState]::NeedsWorkTranslation) {
                        $stateNode.SetAttribute('state', 'needs-adaptation');
                        break;
                    }
                    ([XlfTranslationState]::Translated) {
                        $stateNode.SetAttribute('state', 'translated');
                        break;
                    }
                }
                break;
            }
        }
    }

    [XlfTranslationState] GetState([System.Xml.XmlNode] $unit) {
        [System.Xml.XmlNode] $stateNode = $this.TryGetStateNode($unit);
        if ($stateNode -and $stateNode.HasAttributes) {
            switch ($this.Version()) {
                "1.2" {
                    [string] $stateValue = $stateNode.GetAttribute('state');
                    if ($stateValue) {
                        switch ($stateValue) {
                            'needs-translation' {
                                return [XlfTranslationState]::MissingTranslation;
                            }
                            'needs-adaptation' {
                                return [XlfTranslationState]::NeedsWorkTranslation;
                            }
                            'translated' {
                                return [XlfTranslationState]::Translated;
                            }
                        }
                    }
                    break;
                }
            }
        }
        return [XlfTranslationState]::MissingTranslation;
    }

    [void] SetState([System.Xml.XmlNode] $unit, [XlfTranslationState] $newTranslationState) {
        [System.Xml.XmlNode] $stateNode = $this.TryGetStateNode($unit);
        if ((-not $stateNode) -and ($this.Version() -eq "1.2")) {
            [System.Xml.XmlNode] $newTargetNode = $this.CreateTargetNode($unit, "", $newTranslationState);
            $this.AppendTargetNode($unit, $newTargetNode);
        } elseif ($stateNode) {
            $this.UpdateStateAttributes($stateNode, $newTranslationState);
        }
    }

    hidden [System.Xml.XmlNode] TryGetStateNode([System.Xml.XmlNode] $unit) {
        [string] $stateNodeTag = 'target';
        switch ($this.Version()) {
            "1.2" {
                $stateNodeTag = 'target';
                break;
            }
        }

        return [XlfDocument]::GetNode($stateNodeTag, $unit);
    }

    [void] SetXliffSyncNote([System.Xml.XmlNode] $unit, [string] $noteText) {
        [xml] $xmlDoc = $this.root.OwnerDocument;
        [System.Xml.XmlElement] $noteNode = $xmlDoc.CreateNode([System.Xml.XmlNodeType]::Element, 'note', $this.root.NamespaceURI);
        [string] $fromAttributeValue = "XLIFF Sync";

        [System.Xml.XmlNode] $notesParent = $unit;
        switch ($this.Version()) {
            "1.2" {
                $noteNode.SetAttribute('from', $fromAttributeValue);
                break;
            }
            Default {
                return;
            }
        }

        $noteNode.SetAttribute('annotates', 'general');
        $noteNode.SetAttribute('priority', '1');
        $noteNode.InnerText = $noteText;

        [System.Xml.XmlNode] $existingNote = $this.GetExistingXliffSyncNote($notesParent);
        [System.Xml.XmlNode] $targetChildNode = $unit.ChildNodes | Where-Object { $_.Name -eq "target" } | Select-Object -First 1;

        if ($existingNote) {
            $notesParent.ReplaceChild($noteNode, $existingNote);
        } elseif ($targetChildNode -and ($this.Version() -eq "1.2")) {
            $unit.InsertAfter($noteNode, $targetChildNode.NextSibling);

            # Add the same whitespace after the note.
            $newWhiteSpaceNode = $this.root.OwnerDocument.ImportNode($targetChildNode.PreviousSibling, $true);
            $unit.InsertAfter($newWhiteSpaceNode, $noteNode);
        } else {
            $notesParent.AppendChild($noteNode);
        }
    }

    [bool] TryDeleteXLIFFSyncNote([System.Xml.XmlNode] $unit) {
        [System.Xml.XmlNode] $notesParent = $unit;
        if (-not $notesParent) {
            return $false;
        }

        [System.Xml.XmlNode] $existingNote = $this.GetExistingXliffSyncNote($notesParent);
        if (-not $existingNote) {
            return $false;
        }

        [System.Xml.XmlNode] $whiteSpaceNode = $existingNote.PreviousSibling;
        $notesParent.RemoveChild($existingNote);
        $notesParent.RemoveChild($whiteSpaceNode);

        return $true;
    }

    hidden [System.Xml.XmlNode] GetExistingXliffSyncNote([System.Xml.XmlNode] $notesParent) {
        if (-not $notesParent) {
            return $null;
        }

        [string] $categoryAttributeName = 'from';
        switch ($this.Version()) {
            "1.2" {
                $categoryAttributeName = 'from';
                break;
            }
        }
        [string] $categoryAttributeValue = "XLIFF Sync";
        return $notesParent.ChildNodes | Where-Object { ($_.name -eq 'note') -and ($_.Attributes) -and ($_.GetAttribute($categoryAttributeName) -eq $categoryAttributeValue) } | Select-Object -First 1;
    }

    [System.Xml.XmlNode] CreateTargetNode([System.Xml.XmlNode] $parentUnit, [string] $translation, [XlfTranslationState] $newTranslationState) {
        [xml] $xmlDoc = $this.root.OwnerDocument;
        [System.Xml.XmlNode] $targetNode = $xmlDoc.CreateNode([System.Xml.XmlNodeType]::Element, "target", $this.root.NamespaceURI);
        $xmlDoc.ImportNode($targetNode, $true);

        if ($this.Version() -eq "1.2") {
            $this.UpdateStateAttributes($targetNode, $newTranslationState);
        }

        if ($translation) {
            $targetNode.InnerText = $translation;
        }

        return $targetNode;
    }

    [void] AppendTargetNode([System.Xml.XmlNode] $unit, [System.Xml.XmlNode] $targetNode) {
        if (-not $unit) {
            return;
        }

        switch ($this.Version()) {
            "1.2" {
                [System.Xml.XmlNode] $sourceChildNode = $unit.ChildNodes | Where-Object { $_.Name -eq "source" } | Select-Object -First 1;
                [System.Xml.XmlNode] $targetChildNode = $unit.ChildNodes | Where-Object { $_.Name -eq "target" } | Select-Object -First 1;

                if ($targetChildNode) {
                    $unit.ReplaceChild($targetNode, $targetChildNode);
                } elseif ($sourceChildNode) {
                    if ($unit.Attributes["xml:space"] -and ($unit.Attributes["xml:space"].Value -eq "preserve")) {
                        $unit.InsertAfter($targetNode, $sourceChildNode.NextSibling);

                        # Add the same whitespace after the target node.
                        $newWhiteSpaceNode = $this.root.OwnerDocument.ImportNode($sourceChildNode.PreviousSibling, $true);
                        $unit.InsertAfter($newWhiteSpaceNode, $targetNode);
                    } else {
                        $unit.InsertAfter($targetNode, $sourceChildNode);
                    }
                } else {
                    $unit.AppendChild($targetNode);
                }
                break;
            }
        }
    }

    [void] DeleteTargetNode([System.Xml.XmlElement] $unitNode) {
        if ($unitNode) {
            [System.Xml.XmlNode] $targetNode = $unitNode.ChildNodes | Where-Object { $_.Name -eq 'target' } | Select-Object -First 1;
            if ($targetNode) {
                $unitNode.RemoveChild($targetNode);
            }
        }
    }

    [boolean] GetUnitNeedsTranslation([System.Xml.XmlNode] $unitNode) {
        [string] $translateAttribute = $unitNode.'translate';
        if ($translateAttribute) {
            return $translateAttribute -eq 'yes';
        }
        return $true;
    }

    [string] GetUnitSourceText([System.Xml.XmlNode] $unitNode) {
        [System.Xml.XmlNode] $sourceNode = [XlfDocument]::GetNode('source', $unitNode);
        if ((-not $sourceNode) -and (-not $sourceNode.HasChildNodes)) {
            return $null;
        }
        return $sourceNode.ChildNodes[0].Value;
    }

    [string] GetUnitTranslation([System.Xml.XmlNode] $unitNode) {
        [System.Xml.XmlNode] $translationNode = [XlfDocument]::GetNode('target', $unitNode);
        if ((-not $translationNode) -and (-not $translationNode.HasChildNodes)) {
            return $null;
        }
        return $translationNode.ChildNodes[0].Value;
    }

    [string] GetUnitDeveloperNote([System.Xml.XmlNode] $unitNode) {
        return $this.GetUnitNoteText($unitNode, $this.developerNoteDesignation);
    }

    [string] GetUnitXliffGeneratorNote([System.Xml.XmlNode] $unitNode) {
        return $this.GetUnitNoteText($unitNode, $this.xliffGeneratorNoteDesignation);
    }

    [string] GetUnitNoteText([System.Xml.XmlNode] $unitNode, [string] $noteDesignation) {
        [System.Xml.XmlNode] $noteNode = $null;

        switch ($this.Version()) {
            "1.2" {
                $noteNode = $unitNode.ChildNodes | Where-Object { ($_.Name -eq "note") -and ($_.'from' -eq $noteDesignation) } | Select-Object -First 1;
                break;
            }
        }

        if ((-not $noteNode) -or (-not $noteNode.InnerText)) {
            return $null;
        }

        return $noteNode.InnerText;
    }

    [string] GetUnitTranslationFromDeveloperNote([System.Xml.XmlNode] $unitNode) {
        [string] $developerNoteText = $this.GetUnitDeveloperNote($unitNode);
        if (-not $developerNoteText) {
            return $null;
        }

        [string[]] $translationEntries = $developerNoteText.Split($this.parseFromDeveloperNoteSeparator);
        [string] $translationText = $null;

        for ($i = 0; $i -lt $translationEntries.Length; $i++) {
            [string] $translationEntry = $translationEntries[$i];
            [int] $trlSepIdx = $translationEntry.IndexOf('=');
            if ($trlSepIdx -lt 0) {
                continue;
            }

            [string] $language = $translationEntry.Substring(0, $trlSepIdx);
            if ($language -eq $this.GetTargetLanguage()) {
                $translationText = $translationEntry.Substring($trlSepIdx + 1);
                break;
            }
        }

        return $translationText;
    }

    [System.Xml.XmlNode[]] TranslationUnitNodes() {
        if ($this.cachedTranslationUnitNodes) {
            return $this.cachedTranslationUnitNodes;
        }

        [System.Xml.XmlNode[]] $transUnits = @();
        if (-not $this.root) {
            return $transUnits;
        }

        switch ($this.Version()) {
            "1.2" {
                [System.Xml.XmlNode] $bodyNode = [XlfDocument]::GetNode('body', $this.root);
                if ($bodyNode) {
                    $unitsInBody = $this.GetTranslationUnitsFromRoot($bodyNode);
                    if ($unitsInBody -and ($unitsInBody.Count -gt 0)) {
                        $transUnits += $unitsInBody;
                    }

                    $unitsInGroups = $this.GetGroupTranslationUnitNodes($bodyNode);
                    if ($unitsInGroups -and ($unitsInGroups.Count -gt 0)) {
                        $transUnits += $unitsInGroups;
                    }
                }
                break;
            }
        }

        $this.cachedTranslationUnitNodes = $transUnits;

        return $transUnits;
    }

    [void] SaveToFilePath([string] $filePath) {
        if (Test-Path $filePath) {
            $filePath = Resolve-Path $filePath
        }
        $this.root.OwnerDocument.Save($filePath);
    }

    hidden [System.Xml.XmlNode[]] GetGroupTranslationUnitNodes([System.Xml.XmlNode] $rootNode) {
        [System.Xml.XmlNode[]] $transUnits = @();
        [System.Xml.XmlNode[]] $groupNodes = $rootNode.ChildNodes | Where-Object {
            $_.Name -eq 'group'
        };
        if (-not $groupNodes) {
            return $transUnits
        }

        $groupNodes | ForEach-Object {
            $unitsInGroup = $this.GetTranslationUnitsFromRoot($_);
            if ($unitsInGroup -and ($unitsInGroup.Count -gt 0)) {
                $transUnits += $unitsInGroup;
            }
        }
        $groupNodes | ForEach-Object {
            $unitsInSubGroups = $this.GetGroupTranslationUnitNodes($_);
            if ($unitsInSubGroups -and ($unitsInSubGroups.Count -gt 0)) {
                $transUnits += $unitsInSubGroups;
            }
        }
        return $transUnits;
    }

    hidden [System.Xml.XmlNode[]] GetTranslationUnitsFromRoot([System.Xml.XmlNode] $rootNode) {
        return $rootNode.ChildNodes | Where-Object {
            $_.Name -eq 'trans-unit'
        };
    }

    hidden static [System.Xml.XmlNode] GetNode([string] $tag, [System.Xml.XmlNode] $node) {
        if (-not $node) {
            return $null;
        }

        if ($node.Name -eq $tag) {
            return $node;
        } else {
            foreach ($member in $node.ChildNodes) {
                [System.Xml.XmlNode] $child = $member;
                $reqNode = [XlfDocument]::GetNode($tag, $child);
                if ($null -ne $reqNode) {
                    return $reqNode;
                }
            }
        }

        return $null;
    }

    static [XlfDocument] CreateCopyFrom([XlfDocument] $baseXlfDoc, [string] $language) {
        [xml] $baseXmlDoc = $baseXlfDoc.root.OwnerDocument;
        [System.Xml.XmlNode] $xmlDecl = $baseXmlDoc.ChildNodes.Item(0);
        [System.Xml.XmlNode] $rootNode = $baseXmlDoc.ChildNodes.Item(1);

        [XlfDocument] $newXfDoc = [XlfDocument]::new();
        [xml] $newXmlDoc = [System.Xml.XmlDocument]::new();
        $newXmlDecl = $newXmlDoc.ImportNode($xmlDecl, $false);
        $newXmlDoc.AppendChild($newXmlDecl);

        $newRootNode = $newXmlDoc.ImportNode($rootNode, $true);
        $newXmlDoc.AppendChild($newRootNode);
        $newXfDoc.root = $newRootNode;

        switch ($baseXlfDoc.Version()) {
            "1.2" {
                $newFileNode = $newRootNode.ChildNodes.Item(0);
                $newFileNode.'target-language' = $language;
                break;
            }
            "2.0" {
                $newRootNode.'trgLang' = $language;
                break;
            }
        }

        return $newXfDoc;
    }

    static [XlfDocument] CreateEmptyDocFrom([XlfDocument] $baseXlfDoc, [string] $language) {
        [xml] $baseXmlDoc = $baseXlfDoc.root.OwnerDocument;
        [System.Xml.XmlNode] $xmlDecl = $baseXmlDoc.ChildNodes.Item(0);
        [System.Xml.XmlNode] $rootNode = $baseXmlDoc.ChildNodes.Item(1);

        [XlfDocument] $newXfDoc = [XlfDocument]::new();
        [xml] $newXmlDoc = [System.Xml.XmlDocument]::new();
        $newXmlDecl = $newXmlDoc.ImportNode($xmlDecl, $false);
        $newXmlDoc.AppendChild($newXmlDecl);

        $newRootNode = $newXmlDoc.ImportNode($rootNode, $false);
        $newXmlDoc.AppendChild($newRootNode);

        switch ($baseXlfDoc.Version()) {
            "1.2" {
                $baseFileNode = $rootNode.ChildNodes.Item(0);
                $newFileNode = $newXmlDoc.ImportNode($baseFileNode, $false);
                $newFileNode.'target-language' = $language;
                $newRootNode.AppendChild($newFileNode);

                [System.Xml.XmlNode] $newBodyNode = $newXmlDoc.CreateNode([System.Xml.XmlNodeType]::Element, "body", $newRootNode.NamespaceURI);
                $newFileNode.AppendChild($newBodyNode);

                [System.Xml.XmlNode] $newGroupNode = $newXmlDoc.CreateNode([System.Xml.XmlNodeType]::Element, "group", $newRootNode.NamespaceURI);
                ([System.Xml.XmlElement] $newGroupNode).SetAttribute('id', 'body');
                $newBodyNode.AppendChild($newGroupNode);
                break;
            }
        }

        $newXfDoc.root = $newRootNode;
        return $newXfDoc;
    }

    hidden static [XlfDocument] LoadFromRootNode([System.Xml.XmlNode] $rootNode) {
        [XlfDocument] $doc = [XlfDocument]::new();
        $doc.root = $rootNode;

        if ($doc.Version() -ne "1.2") {
            throw "Currently this module only supports XLIFF 1.2 Files. Support for XLIFF 2.0 will be added later.";
        }

        return $doc;
    }

    static [XlfDocument] LoadFromXmlDocument([xml] $xmlDoc) {
        [System.Xml.XmlNode] $xmlDecl = $xmlDoc.ChildNodes.Item(0);
        $xmlDecl.'encoding' = $xmlDecl.'encoding'.ToUpper();

        [System.Xml.XmlNode] $rootNode = $xmlDoc.ChildNodes.Item(1);

        return [XlfDocument]::LoadFromRootNode($rootNode);
    }

    static [XlfDocument] LoadFromPath([string] $filePath) {
        $ErrorActionPreference = 'Stop'
        $filePath = Resolve-Path $filePath
        [xml] $fileContentXml = (New-Object System.Xml.XmlDocument);
        $fileContentXml.Load($filePath);
        return [XlfDocument]::LoadFromXmlDocument($fileContentXml);
    }

    [void] AddFromPath([string] $filePath) {
        [xml] $fileContentXml = (New-Object System.Xml.XmlDocument);
        $fileContentXml.Load($filePath);
        $xlfDoc = [XlfDocument]::LoadFromXmlDocument($fileContentXml);

        if ($null -ne $this.root) {
            [xml] $xmlDoc = $this.root.OwnerDocument;
            $group = [XlfDocument]::GetNode('group', $xlfDoc.root);
            $imported = $xmlDoc.ImportNode($group, $true);
            $groupImported = [XlfDocument]::GetNode('group', $this.root);
            $groupImported.AppendChild($imported);
        } else {
            $this.root = $xlfDoc.root;
        }
    }
}

enum XlfTranslationState {
    MissingTranslation = 0
    NeedsWorkTranslation = 1
    Translated = 10
}