diff --git a/src/Microsoft.DotNet.XliffTasks.Tests/ResxDocumentTests.cs b/src/Microsoft.DotNet.XliffTasks.Tests/ResxDocumentTests.cs index 700f086d1d2..bf3fa86714f 100644 --- a/src/Microsoft.DotNet.XliffTasks.Tests/ResxDocumentTests.cs +++ b/src/Microsoft.DotNet.XliffTasks.Tests/ResxDocumentTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.IO; using XliffTasks.Model; @@ -49,34 +50,51 @@ public void BasicLoadAndTranslate() } [Fact] - public void RewriteFileReferenceToAbsoluteInDestinyFolder() + public void RewriteFileReferenceToRelativePathFromOutputFolder() { - string sourceFolder = Directory.GetCurrentDirectory(); - string expectedAbsoluteLocation = Path.Combine( - Directory.GetCurrentDirectory(), - @"Resources\Package.ico".Replace('\\', Path.DirectorySeparatorChar)); + // Simulates: source .resx is in src/MyProject/, translated output .resx is in artifacts/obj/MyProject.xlf/ + // The resource file at src/MyProject/Resources/Package.ico should be referenced as + // a relative path from the output directory. + string tempBase = Path.Combine(Path.GetTempPath(), "xliff-test-" + Path.GetRandomFileName()); + string sourceFolder = Path.Combine(tempBase, "src", "MyProject"); + string outputFolder = Path.Combine(tempBase, "artifacts", "obj", "MyProject.xlf"); + string sourceFullPath = Path.Combine(sourceFolder, "Resources.resx"); + string outputFullPath = Path.Combine(outputFolder, "MyProject.resx"); + + string resourceRelativePath = Path.Combine("Resources", "Package.ico"); + string resourceAbsolutePath = Path.GetFullPath(Path.Combine(sourceFolder, resourceRelativePath)); + + // Compute expected relative path from output directory to resource file + Uri fromUri = new Uri(outputFolder.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar); + Uri toUri = new Uri(resourceAbsolutePath); + string expectedRelativePath = Uri.UnescapeDataString(fromUri.MakeRelativeUri(toUri).ToString()) + .Replace('/', Path.DirectorySeparatorChar); + string source = -@" +$@" - Resources\Package.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + {resourceRelativePath};System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a "; string expectedTranslation = -@" +$@" - ABSOLUTEPATH;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + {expectedRelativePath};System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a -".Replace("ABSOLUTEPATH", expectedAbsoluteLocation); +"; ResxDocument document = new(); StringWriter writer = new(); document.Load(new StringReader(source)); - document.RewriteRelativePathsToAbsolute( - Path.Combine(sourceFolder, "Resources.resx")); + document.RewriteRelativePathsForOutputPath(sourceFullPath, outputFullPath); document.Save(writer); AssertEx.EqualIgnoringLineEndings(expectedTranslation, writer.ToString()); + + // The path in the output must NOT be absolute — it must be relative, + // so the translated .resx remains valid after the repo is renamed or moved. + Assert.DoesNotContain(tempBase, writer.ToString()); } diff --git a/src/Microsoft.DotNet.XliffTasks.Tests/VsctDocumentTests.cs b/src/Microsoft.DotNet.XliffTasks.Tests/VsctDocumentTests.cs index b9ee1562dc2..76b9806b578 100644 --- a/src/Microsoft.DotNet.XliffTasks.Tests/VsctDocumentTests.cs +++ b/src/Microsoft.DotNet.XliffTasks.Tests/VsctDocumentTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.IO; using XliffTasks.Model; @@ -71,35 +72,47 @@ public void BasicLoadAndTranslate() } [Fact] - public void RewriteHrefOfImageToAbsoluteInDestinyFolder() + public void RewriteHrefOfImageToRelativePathFromOutputFolder() { - string sourceFolder = Directory.GetCurrentDirectory(); - string expectedAbsoluteLocation = Path.Combine( - Directory.GetCurrentDirectory(), - @"Resources\Images.png".Replace('\\', Path.DirectorySeparatorChar)); + // Simulates: source .vsct is in src/MyProject/, translated output is in artifacts/obj/MyProject.xlf/ + string tempBase = Path.Combine(Path.GetTempPath(), "xliff-vsct-test-" + Path.GetRandomFileName()); + string sourceFolder = Path.Combine(tempBase, "src", "MyProject"); + string outputFolder = Path.Combine(tempBase, "artifacts", "obj", "MyProject.xlf"); + string sourceFullPath = Path.Combine(sourceFolder, "MyPackage.vsct"); + string outputFullPath = Path.Combine(outputFolder, "MyPackage.vsct"); + + string resourceRelativePath = Path.Combine("Resources", "Images.png"); + string resourceAbsolutePath = Path.GetFullPath(Path.Combine(sourceFolder, resourceRelativePath)); + + Uri fromUri = new Uri(outputFolder.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar); + Uri toUri = new Uri(resourceAbsolutePath); + string expectedRelativePath = Uri.UnescapeDataString(fromUri.MakeRelativeUri(toUri).ToString()) + .Replace('/', Path.DirectorySeparatorChar); string source = -@" +$@" - + "; string expectedTranslation = -@" +$@" - + -".Replace("ABSOLUTEPATH", expectedAbsoluteLocation); +"; VsctDocument document = new(); StringWriter writer = new(); document.Load(new StringReader(source)); - document.RewriteRelativePathsToAbsolute( - Path.Combine(sourceFolder, "Resources.resx")); + document.RewriteRelativePathsForOutputPath(sourceFullPath, outputFullPath); document.Save(writer); AssertEx.EqualIgnoringLineEndings(expectedTranslation, writer.ToString()); + + // The path in the output must NOT be absolute + Assert.DoesNotContain(tempBase, writer.ToString()); } [Fact] @@ -112,11 +125,12 @@ public void DoesNotNullReferenceWhenNoHRef() "; + string sourceFullPath = Path.Combine(Directory.GetCurrentDirectory(), "Resources.vsct"); + VsctDocument document = new(); StringWriter writer = new(); document.Load(new StringReader(source)); - document.RewriteRelativePathsToAbsolute( - Path.Combine(Directory.GetCurrentDirectory(), "Resources.resx")); + document.RewriteRelativePathsForOutputPath(sourceFullPath, sourceFullPath); document.Save(writer); AssertEx.EqualIgnoringLineEndings(source, writer.ToString()); diff --git a/src/Microsoft.DotNet.XliffTasks/Model/ResxDocument.cs b/src/Microsoft.DotNet.XliffTasks/Model/ResxDocument.cs index 5aa959f076b..12e023b39ed 100644 --- a/src/Microsoft.DotNet.XliffTasks/Model/ResxDocument.cs +++ b/src/Microsoft.DotNet.XliffTasks/Model/ResxDocument.cs @@ -54,8 +54,11 @@ protected override IEnumerable GetTranslatableNodes() } } - public override void RewriteRelativePathsToAbsolute(string sourceFullPath) + public override void RewriteRelativePathsForOutputPath(string sourceFullPath, string outputFullPath) { + string sourceDir = Path.GetDirectoryName(sourceFullPath); + string outputDir = Path.GetDirectoryName(outputFullPath); + foreach (XElement node in Document.Descendants("data")) { if (node.Attribute("type")?.Value == "System.Resources.ResXFileRef, System.Windows.Forms") @@ -64,8 +67,8 @@ public override void RewriteRelativePathsToAbsolute(string sourceFullPath) string[] splitRelativePathAndSerializedType = valueNodeOfFileRef.Value.Split(';'); string resourceRelativePath = splitRelativePathAndSerializedType[0].Replace('\\', Path.DirectorySeparatorChar); - string absolutePath = Path.Combine(Path.GetDirectoryName(sourceFullPath), resourceRelativePath); - splitRelativePathAndSerializedType[0] = absolutePath; + string absoluteResourcePath = Path.GetFullPath(Path.Combine(sourceDir, resourceRelativePath)); + splitRelativePathAndSerializedType[0] = MakeRelativePath(outputDir, absoluteResourcePath); valueNodeOfFileRef.Value = string.Join(";", splitRelativePathAndSerializedType); } diff --git a/src/Microsoft.DotNet.XliffTasks/Model/TranslatableDocument.cs b/src/Microsoft.DotNet.XliffTasks/Model/TranslatableDocument.cs index b72ce66f589..a9e923254ca 100644 --- a/src/Microsoft.DotNet.XliffTasks/Model/TranslatableDocument.cs +++ b/src/Microsoft.DotNet.XliffTasks/Model/TranslatableDocument.cs @@ -62,11 +62,39 @@ public void Translate(IReadOnlyDictionary translations) } // rewrite nodes that point to external files (used often for icons, etc.) - // these will have relative paths adjusted to absolute path. - public virtual void RewriteRelativePathsToAbsolute(string sourceFullPath) + // these will have relative paths adjusted to a path relative to the output file. + public virtual void RewriteRelativePathsForOutputPath(string sourceFullPath, string outputFullPath) { } + /// + /// Computes a relative path from to . + /// Falls back to an absolute path if the paths are on different drives. + /// + protected static string MakeRelativePath(string fromDirectory, string toPath) + { + // Ensure fromDirectory ends with a separator so Uri treats it as a directory + if (!fromDirectory.EndsWith(Path.DirectorySeparatorChar) && + !fromDirectory.EndsWith(Path.AltDirectorySeparatorChar)) + { + fromDirectory += Path.DirectorySeparatorChar; + } + + Uri fromUri = new Uri(fromDirectory); + Uri toUri = new Uri(toPath); + + // If on different drives (Windows), fall back to absolute path + if (fromUri.Scheme != toUri.Scheme || fromUri.Host != toUri.Host) + { + return toPath; + } + + Uri relativeUri = fromUri.MakeRelativeUri(toUri); + string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); + + return relativePath.Replace('/', Path.DirectorySeparatorChar); + } + protected abstract void LoadCore(TextReader reader); protected abstract void SaveCore(TextWriter writer); diff --git a/src/Microsoft.DotNet.XliffTasks/Model/VsctDocument.cs b/src/Microsoft.DotNet.XliffTasks/Model/VsctDocument.cs index 6bdaeedbd90..adcf36b15c5 100644 --- a/src/Microsoft.DotNet.XliffTasks/Model/VsctDocument.cs +++ b/src/Microsoft.DotNet.XliffTasks/Model/VsctDocument.cs @@ -74,8 +74,11 @@ private HashSet FindNonUniqueIds() return conflictingIds; } - public override void RewriteRelativePathsToAbsolute(string sourceFullPath) + public override void RewriteRelativePathsForOutputPath(string sourceFullPath, string outputFullPath) { + string sourceDir = Path.GetDirectoryName(sourceFullPath); + string outputDir = Path.GetDirectoryName(outputFullPath); + foreach (XElement imageTag in Document.Descendants(Document.Root.Name.Namespace + "Bitmap")) { XAttribute hrefAttribute = imageTag.Attribute("href"); @@ -84,9 +87,8 @@ public override void RewriteRelativePathsToAbsolute(string sourceFullPath) { string resourceRelativePath = hrefAttribute.Value.Replace('\\', Path.DirectorySeparatorChar); - string absolutePath = Path.Combine(Path.GetDirectoryName(sourceFullPath), resourceRelativePath); - - imageTag.Attribute("href").Value = absolutePath; + string absoluteResourcePath = Path.GetFullPath(Path.Combine(sourceDir, resourceRelativePath)); + imageTag.Attribute("href").Value = MakeRelativePath(outputDir, absoluteResourcePath); } } } diff --git a/src/Microsoft.DotNet.XliffTasks/Tasks/TranslateSource.cs b/src/Microsoft.DotNet.XliffTasks/Tasks/TranslateSource.cs index 1e9aeca0ebc..7243135549d 100644 --- a/src/Microsoft.DotNet.XliffTasks/Tasks/TranslateSource.cs +++ b/src/Microsoft.DotNet.XliffTasks/Tasks/TranslateSource.cs @@ -38,7 +38,7 @@ protected override void ExecuteCore() Directory.CreateDirectory(Path.GetDirectoryName(translatedFullPath)); - sourceDocument.RewriteRelativePathsToAbsolute(Path.GetFullPath(sourcePath)); + sourceDocument.RewriteRelativePathsForOutputPath(Path.GetFullPath(sourcePath), Path.GetFullPath(translatedFullPath)); sourceDocument.Save(translatedFullPath); } }