diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExtendedJavascriptEncoder.cs b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExtendedJavascriptEncoder.cs new file mode 100644 index 00000000000..ebc90ba5154 --- /dev/null +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExtendedJavascriptEncoder.cs @@ -0,0 +1,56 @@ +// 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.Buffers; +using System.Text; +using System.Text.Encodings.Web; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core +{ + internal class ExtendedJavascriptEncoder : JavaScriptEncoder + { + public override int MaxOutputCharactersPerInputCharacter => UnsafeRelaxedJsonEscaping.MaxOutputCharactersPerInputCharacter; + + public override unsafe int FindFirstCharacterToEncode(char* text, int textLength) + { + ReadOnlySpan input = new ReadOnlySpan(text, textLength); + int idx = 0; + + while (Rune.DecodeFromUtf16(input.Slice(idx), out Rune result, out int charsConsumed) == OperationStatus.Done) + { + if (WillEncode(result.Value)) + { + // This character needs to be escaped. Break out. + break; + } + idx += charsConsumed; + } + + if (idx == input.Length) + { + // None of the characters in the string needs to be escaped. + return -1; + } + return idx; + } + + public override bool WillEncode(int unicodeScalar) + { + if (unicodeScalar == 0x00A0) + { + // Don't escape no-break space. + return false; + } + else + { + return UnsafeRelaxedJsonEscaping.WillEncode(unicodeScalar); + } + } + + public override unsafe bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten) + { + return UnsafeRelaxedJsonEscaping.TryEncodeUnicodeScalar(unicodeScalar, buffer, bufferLength, out numberOfCharactersWritten); + } + } +} diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.Designer.cs b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.Designer.cs index b8d8f17091e..25b1dac64a1 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.Designer.cs +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.Designer.cs @@ -79,7 +79,7 @@ internal static string stringExtractor_log_jsonElementExcluded { } /// - /// Looks up a localized string similar to Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children.. + /// Looks up a localized string similar to Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children.. /// internal static string stringExtractor_log_jsonKeyIsNotUnique { get { @@ -88,7 +88,7 @@ internal static string stringExtractor_log_jsonKeyIsNotUnique { } /// - /// Looks up a localized string similar to Json element "{0}" must have a member "{1}".. + /// Looks up a localized string similar to Json element '{0}' must have a member '{1}'.. /// internal static string stringExtractor_log_jsonMemberIsMissing { get { @@ -106,7 +106,16 @@ internal static string stringExtractor_log_skippingAlreadyAddedElement { } /// - /// Looks up a localized string similar to "Failed to read the existing strings from "{0}". + /// Looks up a localized string similar to The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}'. + /// + internal static string stringUpdater_log_dataIsUnchanged { + get { + return ResourceManager.GetString("stringUpdater_log_dataIsUnchanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to read the existing strings from '{0}'. /// internal static string stringUpdater_log_failedToReadLocFile { get { @@ -115,7 +124,7 @@ internal static string stringUpdater_log_failedToReadLocFile { } /// - /// Looks up a localized string similar to Loading existing localizations from file "{0}". + /// Looks up a localized string similar to Loading existing localizations from file '{0}'. /// internal static string stringUpdater_log_loadingLocFile { get { @@ -124,7 +133,16 @@ internal static string stringUpdater_log_loadingLocFile { } /// - /// Looks up a localized string similar to "Opening the following templatestrings.json file for writing: "{0}". + /// Looks up a localized string similar to The file already contains a localized string for key '{0}'. The old value will be preserved.. + /// + internal static string stringUpdater_log_localizedStringAlreadyExists { + get { + return ResourceManager.GetString("stringUpdater_log_localizedStringAlreadyExists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Opening the following templatestrings.json file for writing: '{0}'. /// internal static string stringUpdater_log_openingTemplatesJson { get { diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.resx b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.resx index 6bd89701f0d..8f5c48a16c7 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.resx +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.resx @@ -126,28 +126,36 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". + Json element '{0}' must have a member '{1}'. {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" + Failed to read the existing strings from '{0}' {0} is a file path. - Loading existing localizations from file "{0}" + Loading existing localizations from file '{0}' + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" + Opening the following templatestrings.json file for writing: '{0}' {0} is a file path. \ No newline at end of file diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Microsoft.TemplateEngine.TemplateLocalizer.Core.csproj b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Microsoft.TemplateEngine.TemplateLocalizer.Core.csproj index 4fd50d6f4c2..ac3cce99ed5 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Microsoft.TemplateEngine.TemplateLocalizer.Core.csproj +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Microsoft.TemplateEngine.TemplateLocalizer.Core.csproj @@ -5,6 +5,7 @@ The core API for Template Localizer tool. true enable + true true true diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateStringUpdater.cs b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateStringUpdater.cs index 9c807a0bdd6..dc8e8722c99 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateStringUpdater.cs +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateStringUpdater.cs @@ -5,7 +5,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Unicode; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -99,23 +101,19 @@ private static async Task SaveTemplateStringsFileAsync( // Allow unescaped characters in the strings. This allows writing "aren't" instead of "aren\u0027t". // This is only considered unsafe in a context where symbols may be interpreted as special characters. // For instance, '<' character should be escaped in html documents where this json will be embedded. - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Encoder = new ExtendedJavascriptEncoder(), Indented = true, }; - logger.LogDebug(LocalizableStrings.stringUpdater_log_openingTemplatesJson, filePath); - using FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write); - using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(fileStream, writerOptions); - jsonWriter.WriteStartObject(); + // Determine what strings to write. If they are identical to the existing ones, no need to make disk IO. + List<(string Key, string Value)> valuesToWrite = new(); foreach (TemplateString templateString in templateStrings) { string? localizedText = null; if (!forceUpdate && (existingStrings?.TryGetValue(templateString.LocalizationKey, out localizedText) ?? false)) { - logger.LogDebug( - "The file already contains a localized string for key \"{0}\". The old value will be preserved.", - templateString.LocalizationKey); + logger.LogDebug( LocalizableStrings.stringUpdater_log_localizedStringAlreadyExists, templateString.LocalizationKey); } else { @@ -125,20 +123,60 @@ private static async Task SaveTemplateStringsFileAsync( localizedText = templateString.Value; } - jsonWriter.WritePropertyName(templateString.LocalizationKey); - jsonWriter.WriteStringValue(localizedText); + valuesToWrite.Add((templateString.LocalizationKey, localizedText!)); // A translation and the related comment should be next to each other. Write the comment now before any other text. string commentKey = "_" + templateString.LocalizationKey + ".comment"; if (existingStrings != null && existingStrings.TryGetValue(commentKey, out string? comment)) { - jsonWriter.WritePropertyName(commentKey); - jsonWriter.WriteStringValue(comment); + valuesToWrite.Add((commentKey, comment)); } } + if (SequenceEqual(valuesToWrite, existingStrings)) + { + // Data appears to be same as before. Don't rewrite it. + // Rewriting the same data causes differences in encoding/BOM etc, which marks files as 'changed' in git. + logger.LogDebug(LocalizableStrings.stringUpdater_log_dataIsUnchanged, filePath); + return; + } + + logger.LogDebug(LocalizableStrings.stringUpdater_log_openingTemplatesJson, filePath); + using FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write); + using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(fileStream, writerOptions); + + jsonWriter.WriteStartObject(); + + foreach ((string key, string value) in valuesToWrite) + { + jsonWriter.WritePropertyName(key); + jsonWriter.WriteStringValue(value); + } + jsonWriter.WriteEndObject(); await jsonWriter.FlushAsync(cancellationToken).ConfigureAwait(false); } + + private static bool SequenceEqual(List<(string, string)> lhs, Dictionary? rhs) + { + if (lhs.Count != (rhs?.Count ?? 0)) + { + return false; + } + + if (rhs != null) + { + foreach ((string key, string value) in lhs) + { + if (!rhs.TryGetValue(key, out string existingValue) + || value != existingValue) + { + return false; + } + } + } + + return true; + } } } diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/Rune.cs b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/Rune.cs new file mode 100644 index 00000000000..cac8ca4f086 --- /dev/null +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/Rune.cs @@ -0,0 +1,542 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Text +{ + // netstandard2.0 compatible implementation of System.Text.Rune. + // Copied from: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Encodings.Web/src/Polyfills/System.Text.Rune.netstandard20.cs + internal readonly struct Rune + { + private const int MaxUtf16CharsPerRune = 2; // supplementary plane code points are encoded as 2 UTF-16 code units + + private const char HighSurrogateStart = '\ud800'; + private const char LowSurrogateStart = '\udc00'; + private const int HighSurrogateRange = 0x3FF; + + private readonly uint _value; + + /// + /// Creates a from the provided Unicode scalar value. + /// + /// + /// If does not represent a value Unicode scalar value. + /// + public Rune(uint value) + { + if (!UnicodeUtility.IsValidUnicodeScalar(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _value = value; + } + + /// + /// Creates a from the provided Unicode scalar value. + /// + /// + /// If does not represent a value Unicode scalar value. + /// + public Rune(int value) + : this((uint)value) + { + } + + // non-validating ctor + private Rune(uint scalarValue, bool unused) + { + UnicodeDebug.AssertIsValidScalar(scalarValue); + _value = scalarValue; + } + + /// + /// A instance that represents the Unicode replacement character U+FFFD. + /// + public static Rune ReplacementChar => UnsafeCreate(UnicodeUtility.ReplacementChar); + + /// + /// Returns true if and only if this scalar value is ASCII ([ U+0000..U+007F ]) + /// and therefore representable by a single UTF-8 code unit. + /// + public bool IsAscii => UnicodeUtility.IsAsciiCodePoint(_value); + + /// + /// Returns true if and only if this scalar value is within the BMP ([ U+0000..U+FFFF ]) + /// and therefore representable by a single UTF-16 code unit. + /// + public bool IsBmp => UnicodeUtility.IsBmpCodePoint(_value); + + /// + /// Returns the length in code units () of the + /// UTF-16 sequence required to represent this scalar value. + /// + /// + /// The return value will be 1 or 2. + /// + public int Utf16SequenceLength + { + get + { + int codeUnitCount = UnicodeUtility.GetUtf16SequenceLength(_value); + Debug.Assert(codeUnitCount > 0 && codeUnitCount <= MaxUtf16CharsPerRune); + return codeUnitCount; + } + } + + /// + /// Returns the Unicode scalar value as an integer. + /// + public int Value => (int)_value; + + public static bool operator ==(Rune left, Rune right) => left._value == right._value; + + public static bool operator !=(Rune left, Rune right) => left._value != right._value; + + public static bool IsControl(Rune value) + { + // Per the Unicode stability policy, the set of control characters + // is forever fixed at [ U+0000..U+001F ], [ U+007F..U+009F ]. No + // characters will ever be added to or removed from the "control characters" + // group. See https://www.unicode.org/policies/stability_policy.html. + + // Logic below depends on Rune.Value never being -1 (since Rune is a validating type) + // 00..1F (+1) => 01..20 (&~80) => 01..20 + // 7F..9F (+1) => 80..A0 (&~80) => 00..20 + + return ((value._value + 1) & ~0x80u) <= 0x20u; + } + + /// + /// Decodes the at the beginning of the provided UTF-16 source buffer. + /// + /// + /// + /// If the source buffer begins with a valid UTF-16 encoded scalar value, returns , + /// and outs via the decoded and via the + /// number of s used in the input buffer to encode the . + /// + /// + /// If the source buffer is empty or contains only a standalone UTF-16 high surrogate character, returns , + /// and outs via and via the length of the input buffer. + /// + /// + /// If the source buffer begins with an ill-formed UTF-16 encoded scalar value, returns , + /// and outs via and via the number of + /// s used in the input buffer to encode the ill-formed sequence. + /// + /// + /// + /// The general calling convention is to call this method in a loop, slicing the buffer by + /// elements on each iteration of the loop. On each iteration of the loop + /// will contain the real scalar value if successfully decoded, or it will contain if + /// the data could not be successfully decoded. This pattern provides convenient automatic U+FFFD substitution of + /// invalid sequences while iterating through the loop. + /// + public static OperationStatus DecodeFromUtf16(ReadOnlySpan source, out Rune result, out int charsConsumed) + { + if (!source.IsEmpty) + { + // First, check for the common case of a BMP scalar value. + // If this is correct, return immediately. + + char firstChar = source[0]; + if (TryCreate(firstChar, out result)) + { + charsConsumed = 1; + return OperationStatus.Done; + } + + // First thing we saw was a UTF-16 surrogate code point. + // Let's optimistically assume for now it's a high surrogate and hope + // that combining it with the next char yields useful results. + + if (1 < (uint)source.Length) + { + char secondChar = source[1]; + if (TryCreate(firstChar, secondChar, out result)) + { + // Success! Formed a supplementary scalar value. + charsConsumed = 2; + return OperationStatus.Done; + } + else + { + // Either the first character was a low surrogate, or the second + // character was not a low surrogate. This is an error. + goto InvalidData; + } + } + else if (!char.IsHighSurrogate(firstChar)) + { + // Quick check to make sure we're not going to report NeedMoreData for + // a single-element buffer where the data is a standalone low surrogate + // character. Since no additional data will ever make this valid, we'll + // report an error immediately. + goto InvalidData; + } + } + + // If we got to this point, the input buffer was empty, or the buffer + // was a single element in length and that element was a high surrogate char. + + charsConsumed = source.Length; + result = ReplacementChar; + return OperationStatus.NeedMoreData; + + InvalidData: + + charsConsumed = 1; // maximal invalid subsequence for UTF-16 is always a single code unit in length + result = ReplacementChar; + return OperationStatus.InvalidData; + } + + /// + /// Attempts to create a from the provided input value. + /// + public static bool TryCreate(char ch, out Rune result) + { + uint extendedValue = ch; + if (!UnicodeUtility.IsSurrogateCodePoint(extendedValue)) + { + result = UnsafeCreate(extendedValue); + return true; + } + else + { + result = default; + return false; + } + } + + /// + /// Attempts to create a from the provided UTF-16 surrogate pair. + /// Returns if the input values don't represent a well-formed UTF-16surrogate pair. + /// + public static bool TryCreate(char highSurrogate, char lowSurrogate, out Rune result) + { + // First, extend both to 32 bits, then calculate the offset of + // each candidate surrogate char from the start of its range. + + uint highSurrogateOffset = (uint)highSurrogate - HighSurrogateStart; + uint lowSurrogateOffset = (uint)lowSurrogate - LowSurrogateStart; + + // This is a single comparison which allows us to check both for validity at once since + // both the high surrogate range and the low surrogate range are the same length. + // If the comparison fails, we call to a helper method to throw the correct exception message. + + if ((highSurrogateOffset | lowSurrogateOffset) <= HighSurrogateRange) + { + // The 0x40u << 10 below is to account for uuuuu = wwww + 1 in the surrogate encoding. + result = UnsafeCreate((highSurrogateOffset << 10) + ((uint)lowSurrogate - LowSurrogateStart) + (0x40u << 10)); + return true; + } + else + { + // Didn't have a high surrogate followed by a low surrogate. + result = default; + return false; + } + } + + /// + /// Decodes the at the beginning of the provided UTF-8 source buffer. + /// + /// + /// + /// If the source buffer begins with a valid UTF-8 encoded scalar value, returns , + /// and outs via the decoded and via the + /// number of s used in the input buffer to encode the . + /// + /// + /// If the source buffer is empty or contains only a partial UTF-8 subsequence, returns , + /// and outs via and via the length of the input buffer. + /// + /// + /// If the source buffer begins with an ill-formed UTF-8 encoded scalar value, returns , + /// and outs via and via the number of + /// s used in the input buffer to encode the ill-formed sequence. + /// + /// + /// + /// The general calling convention is to call this method in a loop, slicing the buffer by + /// elements on each iteration of the loop. On each iteration of the loop + /// will contain the real scalar value if successfully decoded, or it will contain if + /// the data could not be successfully decoded. This pattern provides convenient automatic U+FFFD substitution of + /// invalid sequences while iterating through the loop. + /// + public static OperationStatus DecodeFromUtf8(ReadOnlySpan source, out Rune result, out int bytesConsumed) + { + // This method follows the Unicode Standard's recommendation for detecting + // the maximal subpart of an ill-formed subsequence. See The Unicode Standard, + // Ch. 3.9 for more details. In summary, when reporting an invalid subsequence, + // it tries to consume as many code units as possible as long as those code + // units constitute the beginning of a longer well-formed subsequence per Table 3-7. + + int index = 0; + + // Try reading input[0]. + + if ((uint)index >= (uint)source.Length) + { + goto NeedsMoreData; + } + + uint tempValue = source[index]; + if (!UnicodeUtility.IsAsciiCodePoint(tempValue)) + { + goto NotAscii; + } + + Finish: + + bytesConsumed = index + 1; + Debug.Assert(1 <= bytesConsumed && bytesConsumed <= 4); // Valid subsequences are always length [1..4] + result = UnsafeCreate(tempValue); + return OperationStatus.Done; + + NotAscii: + + // Per Table 3-7, the beginning of a multibyte sequence must be a code unit in + // the range [C2..F4]. If it's outside of that range, it's either a standalone + // continuation byte, or it's an overlong two-byte sequence, or it's an out-of-range + // four-byte sequence. + + if (!UnicodeUtility.IsInRangeInclusive(tempValue, 0xC2, 0xF4)) + { + goto FirstByteInvalid; + } + + tempValue = (tempValue - 0xC2) << 6; + + // Try reading input[1]. + + index++; + if ((uint)index >= (uint)source.Length) + { + goto NeedsMoreData; + } + + // Continuation bytes are of the form [10xxxxxx], which means that their two's + // complement representation is in the range [-65..-128]. This allows us to + // perform a single comparison to see if a byte is a continuation byte. + + int thisByteSignExtended = (sbyte)source[index]; + if (thisByteSignExtended >= -64) + { + goto Invalid; + } + + tempValue += (uint)thisByteSignExtended; + tempValue += 0x80; // remove the continuation byte marker + tempValue += (0xC2 - 0xC0) << 6; // remove the leading byte marker + + if (tempValue < 0x0800) + { + Debug.Assert(UnicodeUtility.IsInRangeInclusive(tempValue, 0x0080, 0x07FF)); + goto Finish; // this is a valid 2-byte sequence + } + + // This appears to be a 3- or 4-byte sequence. Since per Table 3-7 we now have + // enough information (from just two code units) to detect overlong or surrogate + // sequences, we need to perform these checks now. + + if (!UnicodeUtility.IsInRangeInclusive(tempValue, ((0xE0 - 0xC0) << 6) + (0xA0 - 0x80), ((0xF4 - 0xC0) << 6) + (0x8F - 0x80))) + { + // The first two bytes were not in the range [[E0 A0]..[F4 8F]]. + // This is an overlong 3-byte sequence or an out-of-range 4-byte sequence. + goto Invalid; + } + + if (UnicodeUtility.IsInRangeInclusive(tempValue, ((0xED - 0xC0) << 6) + (0xA0 - 0x80), ((0xED - 0xC0) << 6) + (0xBF - 0x80))) + { + // This is a UTF-16 surrogate code point, which is invalid in UTF-8. + goto Invalid; + } + + if (UnicodeUtility.IsInRangeInclusive(tempValue, ((0xF0 - 0xC0) << 6) + (0x80 - 0x80), ((0xF0 - 0xC0) << 6) + (0x8F - 0x80))) + { + // This is an overlong 4-byte sequence. + goto Invalid; + } + + // The first two bytes were just fine. We don't need to perform any other checks + // on the remaining bytes other than to see that they're valid continuation bytes. + + // Try reading input[2]. + + index++; + if ((uint)index >= (uint)source.Length) + { + goto NeedsMoreData; + } + + thisByteSignExtended = (sbyte)source[index]; + if (thisByteSignExtended >= -64) + { + goto Invalid; // this byte is not a UTF-8 continuation byte + } + + tempValue <<= 6; + tempValue += (uint)thisByteSignExtended; + tempValue += 0x80; // remove the continuation byte marker + tempValue -= (0xE0 - 0xC0) << 12; // remove the leading byte marker + + if (tempValue <= 0xFFFF) + { + Debug.Assert(UnicodeUtility.IsInRangeInclusive(tempValue, 0x0800, 0xFFFF)); + goto Finish; // this is a valid 3-byte sequence + } + + // Try reading input[3]. + + index++; + if ((uint)index >= (uint)source.Length) + { + goto NeedsMoreData; + } + + thisByteSignExtended = (sbyte)source[index]; + if (thisByteSignExtended >= -64) + { + goto Invalid; // this byte is not a UTF-8 continuation byte + } + + tempValue <<= 6; + tempValue += (uint)thisByteSignExtended; + tempValue += 0x80; // remove the continuation byte marker + tempValue -= (0xF0 - 0xE0) << 18; // remove the leading byte marker + + UnicodeDebug.AssertIsValidSupplementaryPlaneScalar(tempValue); + goto Finish; // this is a valid 4-byte sequence + + FirstByteInvalid: + + index = 1; // Invalid subsequences are always at least length 1. + + Invalid: + + Debug.Assert(1 <= index && index <= 3); // Invalid subsequences are always length 1..3 + bytesConsumed = index; + result = ReplacementChar; + return OperationStatus.InvalidData; + + NeedsMoreData: + + Debug.Assert(0 <= index && index <= 3); // Incomplete subsequences are always length 0..3 + bytesConsumed = index; + result = ReplacementChar; + return OperationStatus.NeedMoreData; + } + + public override bool Equals(object? obj) => (obj is Rune other) && Equals(other); + + public bool Equals(Rune other) => this == other; + + public override int GetHashCode() => Value; + + /// + /// Encodes this to a UTF-16 destination buffer. + /// + /// The buffer to which to write this value as UTF-16. + /// + /// The number of s written to , + /// or 0 if the destination buffer is not large enough to contain the output. + /// True if the value was written to the buffer; otherwise, false. + public bool TryEncodeToUtf16(Span destination, out int charsWritten) + { + if (destination.Length >= 1) + { + if (IsBmp) + { + destination[0] = (char)_value; + charsWritten = 1; + return true; + } + else if (destination.Length >= 2) + { + UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar(_value, out destination[0], out destination[1]); + charsWritten = 2; + return true; + } + } + + // Destination buffer not large enough + + charsWritten = default; + return false; + } + + /// + /// Encodes this to a destination buffer as UTF-8 bytes. + /// + /// The buffer to which to write this value as UTF-8. + /// + /// The number of s written to , + /// or 0 if the destination buffer is not large enough to contain the output. + /// True if the value was written to the buffer; otherwise, false. + public bool TryEncodeToUtf8(Span destination, out int bytesWritten) + { + // The bit patterns below come from the Unicode Standard, Table 3-6. + + if (destination.Length >= 1) + { + if (IsAscii) + { + destination[0] = (byte)_value; + bytesWritten = 1; + return true; + } + + if (destination.Length >= 2) + { + if (_value <= 0x7FFu) + { + // Scalar 00000yyy yyxxxxxx -> bytes [ 110yyyyy 10xxxxxx ] + destination[0] = (byte)((_value + (0b110u << 11)) >> 6); + destination[1] = (byte)((_value & 0x3Fu) + 0x80u); + bytesWritten = 2; + return true; + } + + if (destination.Length >= 3) + { + if (_value <= 0xFFFFu) + { + // Scalar zzzzyyyy yyxxxxxx -> bytes [ 1110zzzz 10yyyyyy 10xxxxxx ] + destination[0] = (byte)((_value + (0b1110 << 16)) >> 12); + destination[1] = (byte)(((_value & (0x3Fu << 6)) >> 6) + 0x80u); + destination[2] = (byte)((_value & 0x3Fu) + 0x80u); + bytesWritten = 3; + return true; + } + + if (destination.Length >= 4) + { + // Scalar 000uuuuu zzzzyyyy yyxxxxxx -> bytes [ 11110uuu 10uuzzzz 10yyyyyy 10xxxxxx ] + destination[0] = (byte)((_value + (0b11110 << 21)) >> 18); + destination[1] = (byte)(((_value & (0x3Fu << 12)) >> 12) + 0x80u); + destination[2] = (byte)(((_value & (0x3Fu << 6)) >> 6) + 0x80u); + destination[3] = (byte)((_value & 0x3Fu) + 0x80u); + bytesWritten = 4; + return true; + } + } + } + } + + // Destination buffer not large enough + + bytesWritten = default; + return false; + } + + /// + /// Creates a without performing validation on the input. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Rune UnsafeCreate(uint scalarValue) => new Rune(scalarValue, false); + } +} diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeDebug.cs b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeDebug.cs new file mode 100644 index 00000000000..bdb2444cd1f --- /dev/null +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeDebug.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Text +{ + // Utility class needed for netstandard2.0 System.Text.Rune implementation. + // Copied from: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeDebug.cs + internal static class UnicodeDebug + { + [Conditional("DEBUG")] + internal static void AssertIsBmpCodePoint(uint codePoint) + { + if (!UnicodeUtility.IsBmpCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid BMP code point."); + } + } + + [Conditional("DEBUG")] + internal static void AssertIsHighSurrogateCodePoint(uint codePoint) + { + if (!UnicodeUtility.IsHighSurrogateCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid UTF-16 high surrogate code point."); + } + } + + [Conditional("DEBUG")] + internal static void AssertIsLowSurrogateCodePoint(uint codePoint) + { + if (!UnicodeUtility.IsLowSurrogateCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid UTF-16 low surrogate code point."); + } + } + + [Conditional("DEBUG")] + internal static void AssertIsValidCodePoint(uint codePoint) + { + if (!UnicodeUtility.IsValidCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid Unicode code point."); + } + } + + [Conditional("DEBUG")] + internal static void AssertIsValidScalar(uint scalarValue) + { + if (!UnicodeUtility.IsValidUnicodeScalar(scalarValue)) + { + Debug.Fail($"The value {ToHexString(scalarValue)} is not a valid Unicode scalar value."); + } + } + + [Conditional("DEBUG")] + internal static void AssertIsValidSupplementaryPlaneScalar(uint scalarValue) + { + if (!UnicodeUtility.IsValidUnicodeScalar(scalarValue) || UnicodeUtility.IsBmpCodePoint(scalarValue)) + { + Debug.Fail($"The value {ToHexString(scalarValue)} is not a valid supplementary plane Unicode scalar value."); + } + } + + /// + /// Formats a code point as the hex string "U+XXXX". + /// + /// + /// The input value doesn't have to be a real code point in the Unicode codespace. It can be any integer. + /// + private static string ToHexString(uint codePoint) + { + return FormattableString.Invariant($"U+{codePoint:X4}"); + } + } +} diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeUtility.cs b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeUtility.cs new file mode 100644 index 00000000000..e9a59551bd9 --- /dev/null +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeUtility.cs @@ -0,0 +1,187 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace System.Text +{ + // Utility class needed for netstandard2.0 System.Text.Rune implementation. + // Copied from: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeUtility.cs + internal static class UnicodeUtility + { + /// + /// The Unicode replacement character U+FFFD. + /// + public const uint ReplacementChar = 0xFFFD; + + /// + /// Returns the Unicode plane (0 through 16, inclusive) which contains this code point. + /// + public static int GetPlane(uint codePoint) + { + UnicodeDebug.AssertIsValidCodePoint(codePoint); + + return (int)(codePoint >> 16); + } + + /// + /// Returns a Unicode scalar value from two code points representing a UTF-16 surrogate pair. + /// + public static uint GetScalarFromUtf16SurrogatePair(uint highSurrogateCodePoint, uint lowSurrogateCodePoint) + { + UnicodeDebug.AssertIsHighSurrogateCodePoint(highSurrogateCodePoint); + UnicodeDebug.AssertIsLowSurrogateCodePoint(lowSurrogateCodePoint); + + // This calculation comes from the Unicode specification, Table 3-5. + // Need to remove the D800 marker from the high surrogate and the DC00 marker from the low surrogate, + // then fix up the "wwww = uuuuu - 1" section of the bit distribution. The code is written as below + // to become just two instructions: shl, lea. + + return (highSurrogateCodePoint << 10) + lowSurrogateCodePoint - ((0xD800U << 10) + 0xDC00U - (1 << 16)); + } + + /// + /// Given a Unicode scalar value, gets the number of UTF-16 code units required to represent this value. + /// + public static int GetUtf16SequenceLength(uint value) + { + UnicodeDebug.AssertIsValidScalar(value); + + value -= 0x10000; // if value < 0x10000, high byte = 0xFF; else high byte = 0x00 + value += (2 << 24); // if value < 0x10000, high byte = 0x01; else high byte = 0x02 + value >>= 24; // shift high byte down + return (int)value; // and return it + } + + /// + /// Decomposes an astral Unicode scalar into UTF-16 high and low surrogate code units. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void GetUtf16SurrogatesFromSupplementaryPlaneScalar(uint value, out char highSurrogateCodePoint, out char lowSurrogateCodePoint) + { + UnicodeDebug.AssertIsValidSupplementaryPlaneScalar(value); + + // This calculation comes from the Unicode specification, Table 3-5. + + highSurrogateCodePoint = (char)((value + ((0xD800u - 0x40u) << 10)) >> 10); + lowSurrogateCodePoint = (char)((value & 0x3FFu) + 0xDC00u); + } + + /// + /// Given a Unicode scalar value, gets the number of UTF-8 code units required to represent this value. + /// + public static int GetUtf8SequenceLength(uint value) + { + UnicodeDebug.AssertIsValidScalar(value); + + // The logic below can handle all valid scalar values branchlessly. + // It gives generally good performance across all inputs, and on x86 + // it's only six instructions: lea, sar, xor, add, shr, lea. + + // 'a' will be -1 if input is < 0x800; else 'a' will be 0 + // => 'a' will be -1 if input is 1 or 2 UTF-8 code units; else 'a' will be 0 + + int a = ((int)value - 0x0800) >> 31; + + // The number of UTF-8 code units for a given scalar is as follows: + // - U+0000..U+007F => 1 code unit + // - U+0080..U+07FF => 2 code units + // - U+0800..U+FFFF => 3 code units + // - U+10000+ => 4 code units + // + // If we XOR the incoming scalar with 0xF800, the chart mutates: + // - U+0000..U+F7FF => 3 code units + // - U+F800..U+F87F => 1 code unit + // - U+F880..U+FFFF => 2 code units + // - U+10000+ => 4 code units + // + // Since the 1- and 3-code unit cases are now clustered, they can + // both be checked together very cheaply. + + value ^= 0xF800u; + value -= 0xF880u; // if scalar is 1 or 3 code units, high byte = 0xFF; else high byte = 0x00 + value += (4 << 24); // if scalar is 1 or 3 code units, high byte = 0x03; else high byte = 0x04 + value >>= 24; // shift high byte down + + // Final return value: + // - U+0000..U+007F => 3 + (-1) * 2 = 1 + // - U+0080..U+07FF => 4 + (-1) * 2 = 2 + // - U+0800..U+FFFF => 3 + ( 0) * 2 = 3 + // - U+10000+ => 4 + ( 0) * 2 = 4 + return (int)value + (a * 2); + } + + /// + /// Returns iff is an ASCII + /// character ([ U+0000..U+007F ]). + /// + /// + /// Per http://www.unicode.org/glossary/#ASCII, ASCII is only U+0000..U+007F. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAsciiCodePoint(uint value) => value <= 0x7Fu; + + /// + /// Returns iff is in the + /// Basic Multilingual Plane (BMP). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBmpCodePoint(uint value) => value <= 0xFFFFu; + + /// + /// Returns iff is a UTF-16 high surrogate code point, + /// i.e., is in [ U+D800..U+DBFF ], inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHighSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xD800U, 0xDBFFU); + + /// + /// Returns iff is between + /// and , inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) => (value - lowerBound) <= (upperBound - lowerBound); + + /// + /// Returns iff is a UTF-16 low surrogate code point, + /// i.e., is in [ U+DC00..U+DFFF ], inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xDC00U, 0xDFFFU); + + /// + /// Returns iff is a UTF-16 surrogate code point, + /// i.e., is in [ U+D800..U+DFFF ], inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xD800U, 0xDFFFU); + + /// + /// Returns iff is a valid Unicode code + /// point, i.e., is in [ U+0000..U+10FFFF ], inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidCodePoint(uint codePoint) => codePoint <= 0x10FFFFU; + + /// + /// Returns iff is a valid Unicode scalar + /// value, i.e., is in [ U+0000..U+D7FF ], inclusive; or [ U+E000..U+10FFFF ], inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidUnicodeScalar(uint value) + { + // This is an optimized check that on x86 is just three instructions: lea, xor, cmp. + // + // After the subtraction operation, the input value is modified as such: + // [ 00000000..0010FFFF ] -> [ FFEF0000..FFFFFFFF ] + // + // We now want to _exclude_ the range [ FFEFD800..FFEFDFFF ] (surrogates) from being valid. + // After the xor, this particular exclusion range becomes [ FFEF0000..FFEF07FF ]. + // + // So now the range [ FFEF0800..FFFFFFFF ] contains all valid code points, + // excluding surrogates. This allows us to perform a single comparison. + + return ((value - 0x110000u) ^ 0xD800u) >= 0xFFEF0800u; + } + } +} diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.cs.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.cs.xlf index 438067262f4..36699c9a2c5 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.cs.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.cs.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - Každý podřízený prvek {0} by měl mít jedinečné ID. V současnosti ID {1} sdílí více podřízených prvků. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Každý podřízený prvek {0} by měl mít jedinečné ID. V současnosti ID {1} sdílí více podřízených prvků. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - Element JSON {0} musí mít člen {1}. + Json element '{0}' must have a member '{1}'. + Element JSON {0} musí mít člen {1}. {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ Následující element ze souboru template.json se přeskočí, protože už byl přidaný do seznamu lokalizovatelných řetězců: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - „Existující řetězce z cesty „{0}“ se nepodařilo přečíst + Failed to read the existing strings from '{0}' + „Existující řetězce z cesty „{0}“ se nepodařilo přečíst {0} is a file path. - Loading existing localizations from file "{0}" - Existující lokalizace se načítají ze souboru „{0}“ + Loading existing localizations from file '{0}' + Existující lokalizace se načítají ze souboru „{0}“ + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - „Následující soubor templatestrings.json se otevírá pro zápis: „{0}“ + Opening the following templatestrings.json file for writing: '{0}' + „Následující soubor templatestrings.json se otevírá pro zápis: „{0}“ {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.de.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.de.xlf index 6a06c6ea885..62ac3d64e39 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.de.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.de.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - Jedes untergeordnete Element von "{0}" muss eine eindeutige ID aufweisen. Zurzeit wird die ID {1} von mehreren untergeordneten Elementen verwendet. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Jedes untergeordnete Element von "{0}" muss eine eindeutige ID aufweisen. Zurzeit wird die ID {1} von mehreren untergeordneten Elementen verwendet. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - Das JSON-Element "{0}" muss einen Member "{1}" aufweisen. + Json element '{0}' must have a member '{1}'. + Das JSON-Element "{0}" muss einen Member "{1}" aufweisen. {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ Das folgende Element in der template.json wird übersprungen, da es bereits der Liste der lokalisierbaren Zeichenfolgen hinzugefügt wurde: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - "Fehler beim Lesen der vorhandenen Zeichenfolgen aus "{0}" + Failed to read the existing strings from '{0}' + "Fehler beim Lesen der vorhandenen Zeichenfolgen aus "{0}" {0} is a file path. - Loading existing localizations from file "{0}" - Vorhandene Lokalisierungen werden aus der Datei "{0}" geladen + Loading existing localizations from file '{0}' + Vorhandene Lokalisierungen werden aus der Datei "{0}" geladen + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - "Öffnen der folgenden templatestrings.json-Datei zum Schreiben: "{0}" + Opening the following templatestrings.json file for writing: '{0}' + "Öffnen der folgenden templatestrings.json-Datei zum Schreiben: "{0}" {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.es.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.es.xlf index b401a9169c3..4ca96d2bad8 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.es.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.es.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - Cada elemento secundario de "{0}" debe tener un identificador único. Actualmente, el identificador "{1}" se comparte entre varios elementos secundarios. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Cada elemento secundario de "{0}" debe tener un identificador único. Actualmente, el identificador "{1}" se comparte entre varios elementos secundarios. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - El elemento JSON "{0}" debe tener un miembro "{1}". + Json element '{0}' must have a member '{1}'. + El elemento JSON "{0}" debe tener un miembro "{1}". {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ Se omitirá el siguiente elemento en el archivo template.json, ya que ya que fue agregado a la lista de cadenas localizables: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - "Error al leer las cadenas existentes de "{0}" + Failed to read the existing strings from '{0}' + "Error al leer las cadenas existentes de "{0}" {0} is a file path. - Loading existing localizations from file "{0}" - Cargando las localizaciones existentes desde el archivo "{0}" + Loading existing localizations from file '{0}' + Cargando las localizaciones existentes desde el archivo "{0}" + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - "Abriendo el siguiente archivo templatestrings.json para escribir: "{0}" + Opening the following templatestrings.json file for writing: '{0}' + "Abriendo el siguiente archivo templatestrings.json para escribir: "{0}" {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.fr.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.fr.xlf index 692471e698e..a2b6c29751f 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.fr.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.fr.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - Chaque enfant de « {0} » doit avoir un ID unique. Actuellement, l’ID « {1} » est partagé par plusieurs enfants. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Chaque enfant de « {0} » doit avoir un ID unique. Actuellement, l’ID « {1} » est partagé par plusieurs enfants. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - L’élément json « {0} » doit avoir un membre « {1} ». + Json element '{0}' must have a member '{1}'. + L’élément json « {0} » doit avoir un membre « {1} ». {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ L’élément suivant dans le fichier template.json est ignoré, car il a déjà été ajouté à la liste des chaînes localisables : {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - « Échec de la lecture des chaînes existantes à partir de « {0} » + Failed to read the existing strings from '{0}' + « Échec de la lecture des chaînes existantes à partir de « {0} » {0} is a file path. - Loading existing localizations from file "{0}" - Chargement des localisations existantes à partir du fichier « {0} » + Loading existing localizations from file '{0}' + Chargement des localisations existantes à partir du fichier « {0} » + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - « Ouverture du fichier templatestrings.json suivant pour l’écriture : « {0} » + Opening the following templatestrings.json file for writing: '{0}' + « Ouverture du fichier templatestrings.json suivant pour l’écriture : « {0} » {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.it.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.it.xlf index 71f83bc5855..76b88219721 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.it.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.it.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - A ogni elemento figlio di "{0}" deve essere assegnato un ID univoco. L'ID "{1}" è attualmente condiviso da più elementi figlio. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + A ogni elemento figlio di "{0}" deve essere assegnato un ID univoco. L'ID "{1}" è attualmente condiviso da più elementi figlio. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - L'elemento JSON "{0}" deve includere un membro "{1}". + Json element '{0}' must have a member '{1}'. + L'elemento JSON "{0}" deve includere un membro "{1}". {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ Il seguente elemento in template.json verrà ignorato perché è già stato aggiunto all'elenco delle stringhe localizzabili: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - "Non è stato possibile leggere le stringhe esistenti da "{0}" + Failed to read the existing strings from '{0}' + "Non è stato possibile leggere le stringhe esistenti da "{0}" {0} is a file path. - Loading existing localizations from file "{0}" - Caricamento localizzazioni esistenti dal file "{0}" + Loading existing localizations from file '{0}' + Caricamento localizzazioni esistenti dal file "{0}" + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - Apertura del seguente file templatestrings.json per la scrittura: "{0}" + Opening the following templatestrings.json file for writing: '{0}' + Apertura del seguente file templatestrings.json per la scrittura: "{0}" {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ja.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ja.xlf index 34661b88a11..59086ab1129 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ja.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ja.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - "{0}" の各子には一意の ID を指定する必要があります。現在、ID "{1}" は複数の子によって共有されています。 + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + "{0}" の各子には一意の ID を指定する必要があります。現在、ID "{1}" は複数の子によって共有されています。 {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - Json 要素 "{0}" には、メンバー "{1}" が必要です。 + Json element '{0}' must have a member '{1}'. + Json 要素 "{0}" には、メンバー "{1}" が必要です。 {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ template.json の次の要素は、ローカライズ可能な文字列の一覧に既に追加されているため、スキップされます: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - ""{0}" から既存の文字列を読み取れませんでした + Failed to read the existing strings from '{0}' + ""{0}" から既存の文字列を読み取れませんでした {0} is a file path. - Loading existing localizations from file "{0}" - ファイル "{0}" から既存のローカライズを読み込んでいます + Loading existing localizations from file '{0}' + ファイル "{0}" から既存のローカライズを読み込んでいます + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - "次の templatestrings.json ファイルを書き込むために開いています: "{0}" + Opening the following templatestrings.json file for writing: '{0}' + "次の templatestrings.json ファイルを書き込むために開いています: "{0}" {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ko.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ko.xlf index 0f8abd3f024..85941ecd400 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ko.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ko.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - "{0}"의 각 자식에는 고유한 ID가 있어야 합니다. 현재 ID "{1}"은(는) 여러 자식이 공유합니다. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + "{0}"의 각 자식에는 고유한 ID가 있어야 합니다. 현재 ID "{1}"은(는) 여러 자식이 공유합니다. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - Json 요소 "{0}"에는 "{1}" 구성원이 있어야 합니다. + Json element '{0}' must have a member '{1}'. + Json 요소 "{0}"에는 "{1}" 구성원이 있어야 합니다. {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ template.json의 다음 요소는 지역화 가능한 문자열 목록에 이미 추가되었으므로 건너뜁니다. {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - “"{0}”에서 기존 문자열을 읽지 못했습니다. + Failed to read the existing strings from '{0}' + “"{0}”에서 기존 문자열을 읽지 못했습니다. {0} is a file path. - Loading existing localizations from file "{0}" - "{0}" 파일에서 기존 지역화를 로드하는 중 + Loading existing localizations from file '{0}' + "{0}" 파일에서 기존 지역화를 로드하는 중 + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - "쓰기를 위해 templatestrings.json 파일을 여는 중: "{0}" + Opening the following templatestrings.json file for writing: '{0}' + "쓰기를 위해 templatestrings.json 파일을 여는 중: "{0}" {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pl.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pl.xlf index fdc8a302595..b6e6d1d6c53 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pl.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pl.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - Każdy element podrzędny „{0}” powinien mieć unikatowy identyfikator. Obecnie identyfikator „{1}” jest współdzielony przez wiele elementów podrzędnych. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Każdy element podrzędny „{0}” powinien mieć unikatowy identyfikator. Obecnie identyfikator „{1}” jest współdzielony przez wiele elementów podrzędnych. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - Element JSON „{0}” musi mieć element członkowski „{1}”. + Json element '{0}' must have a member '{1}'. + Element JSON „{0}” musi mieć element członkowski „{1}”. {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ Następujący element w pliku template.json zostanie pominięty, ponieważ został już dodany do listy tłumaczonych ciągów: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - „Nie można odczytać istniejących ciągów z »{0}« ” + Failed to read the existing strings from '{0}' + „Nie można odczytać istniejących ciągów z »{0}« ” {0} is a file path. - Loading existing localizations from file "{0}" - Ładowanie istniejących lokalizacji z pliku „{0}” + Loading existing localizations from file '{0}' + Ładowanie istniejących lokalizacji z pliku „{0}” + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - "Otwieranie następującego pliku templatestrings.json do zapisu: »{0}« ” + Opening the following templatestrings.json file for writing: '{0}' + "Otwieranie następującego pliku templatestrings.json do zapisu: »{0}« ” {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pt-BR.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pt-BR.xlf index 8e568a684e5..fe0a3decc6d 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pt-BR.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pt-BR.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - Cada elemento filho de "{0}" deve ter um id único. Atualmente, o id "{1}" é compartilhado por vários filhos. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Cada elemento filho de "{0}" deve ter um id único. Atualmente, o id "{1}" é compartilhado por vários filhos. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - O elemento Json "{0}" deve ter um membro "{1}". + Json element '{0}' must have a member '{1}'. + O elemento Json "{0}" deve ter um membro "{1}". {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ O seguinte elemento no template.json será ignorado, pois já foi adicionado à lista de cadeia de caracteres localizáveis: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - "Falha na leitura das cadeias de caracteres existentes a partir do "{0}". + Failed to read the existing strings from '{0}' + "Falha na leitura das cadeias de caracteres existentes a partir do "{0}". {0} is a file path. - Loading existing localizations from file "{0}" - Carregando as localizações existentes a partir do arquivo "{0}". + Loading existing localizations from file '{0}' + Carregando as localizações existentes a partir do arquivo "{0}". + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - "Abrindo o arquivo templatestrings.json a seguir para escrita: "{0}" + Opening the following templatestrings.json file for writing: '{0}' + "Abrindo o arquivo templatestrings.json a seguir para escrita: "{0}" {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ru.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ru.xlf index 2deb39fa9b2..8f0b6b83d20 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ru.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ru.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - У каждого дочернего элемента "{0}" должен быть уникальный идентификатор. Сейчас идентификатор "{1}" используется несколькими дочерними элементами. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + У каждого дочернего элемента "{0}" должен быть уникальный идентификатор. Сейчас идентификатор "{1}" используется несколькими дочерними элементами. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - У элемента JSON "{0}" должен быть член "{1}". + Json element '{0}' must have a member '{1}'. + У элемента JSON "{0}" должен быть член "{1}". {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ Следующий элемент в файле template.json будет пропущен, так как он уже добавлен в список локализуемых строк: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - "Не удалось прочитать существующие строки из "{0} " + Failed to read the existing strings from '{0}' + "Не удалось прочитать существующие строки из "{0} " {0} is a file path. - Loading existing localizations from file "{0}" - Загрузка существующих локализаций из файла "{0}" + Loading existing localizations from file '{0}' + Загрузка существующих локализаций из файла "{0}" + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - "Открытие следующего файла templatestrings.json для записи: "{0}" + Opening the following templatestrings.json file for writing: '{0}' + "Открытие следующего файла templatestrings.json для записи: "{0}" {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.tr.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.tr.xlf index 415ea5f33a2..8bdee6f7ab8 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.tr.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.tr.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - Her "{0}" alt öğesi benzersiz bir kimliğe sahip olmalıdır. Şu anda "{1}" kimliğini birden çok alt öğe paylaşıyor. + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Her "{0}" alt öğesi benzersiz bir kimliğe sahip olmalıdır. Şu anda "{1}" kimliğini birden çok alt öğe paylaşıyor. {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - "{0}" JSON öğesi bir üye "{1}" içermelidir. + Json element '{0}' must have a member '{1}'. + "{0}" JSON öğesi bir üye "{1}" içermelidir. {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ template.json dosyasındaki şu öğe yerelleştirilebilir dizeler listesine zaten eklendiğinden atlanacak: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - "{0}" içindeki mevcut dizeler okunamadı + Failed to read the existing strings from '{0}' + "{0}" içindeki mevcut dizeler okunamadı {0} is a file path. - Loading existing localizations from file "{0}" - "{0}" dosyasındaki mevcut yerelleştirmeler yükleniyor + Loading existing localizations from file '{0}' + "{0}" dosyasındaki mevcut yerelleştirmeler yükleniyor + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - "Yazma için şu templatestrings.json dosyası açılıyor: "{0} " + Opening the following templatestrings.json file for writing: '{0}' + "Yazma için şu templatestrings.json dosyası açılıyor: "{0} " {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hans.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hans.xlf index 72a08a06eb2..52ffe40eacb 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hans.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hans.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - “{0}”的每个子级应具有唯一 id。当前,id“{1}”由多个子级共享。 + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + “{0}”的每个子级应具有唯一 id。当前,id“{1}”由多个子级共享。 {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - Json 元素“{0}”必须具有成员“{1}”。 + Json element '{0}' must have a member '{1}'. + Json 元素“{0}”必须具有成员“{1}”。 {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ 由于已添加到可本地化的字符串列表,系统将会跳过 template.json 中的以下元素: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - “从“{0}”中读取现有字符串失败 + Failed to read the existing strings from '{0}' + “从“{0}”中读取现有字符串失败 {0} is a file path. - Loading existing localizations from file "{0}" - 正在从文件”{0}“加载现有本地化 + Loading existing localizations from file '{0}' + 正在从文件”{0}“加载现有本地化 + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - “打开以下 templatestrings.json 文件用于写入:“{0}” + Opening the following templatestrings.json file for writing: '{0}' + “打开以下 templatestrings.json 文件用于写入:“{0}” {0} is a file path. diff --git a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hant.xlf b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hant.xlf index d412f97ce14..94db6f59d6a 100644 --- a/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hant.xlf +++ b/src/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hant.xlf @@ -13,14 +13,14 @@ {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. - Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children. - "{0}" 的每個子項都應該有唯一識別碼。目前有多個子項共用識別碼 "{1}"。 + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + "{0}" 的每個子項都應該有唯一識別碼。目前有多個子項共用識別碼 "{1}"。 {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" {1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. - Json element "{0}" must have a member "{1}". - Json 元素 "{0}" 必須有成員 "{1}"。 + Json element '{0}' must have a member '{1}'. + Json 元素 "{0}" 必須有成員 "{1}"。 {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. @@ -28,19 +28,29 @@ 因為已將下列元素新增至可當地語系化的字串清單,將略過 template.json 中的下列元素: {0} {0} is a string similar to "postActions/0/manualInstructions/2/text" + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + - "Failed to read the existing strings from "{0}" - 「無法從 "{0}" 讀取現有字串 + Failed to read the existing strings from '{0}' + 「無法從 "{0}" 讀取現有字串 {0} is a file path. - Loading existing localizations from file "{0}" - 正在從檔案 "{0}" 載入現有的當地語系化 + Loading existing localizations from file '{0}' + 正在從檔案 "{0}" 載入現有的當地語系化 + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + The file already contains a localized string for key '{0}'. The old value will be preserved. {0} is a file path. - "Opening the following templatestrings.json file for writing: "{0}" - 「正在開啟下列 templatestrings.json 檔案以進行寫入: "{0}" + Opening the following templatestrings.json file for writing: '{0}' + 「正在開啟下列 templatestrings.json 檔案以進行寫入: "{0}" {0} is a file path. diff --git a/test/Microsoft.TemplateEngine.Tasks.IntegrationTests/LocalizeTemplateTests.cs b/test/Microsoft.TemplateEngine.Tasks.IntegrationTests/LocalizeTemplateTests.cs index 68cc64744b0..a97f3d5d9c6 100644 --- a/test/Microsoft.TemplateEngine.Tasks.IntegrationTests/LocalizeTemplateTests.cs +++ b/test/Microsoft.TemplateEngine.Tasks.IntegrationTests/LocalizeTemplateTests.cs @@ -124,7 +124,7 @@ public void CanRunTaskAndDetectError() .Should() .Fail() .And.HaveStdOutContaining("Build FAILED.") - .And.HaveStdOutContaining("error : Each child of \"//postActions\" should have a unique id"); + .And.HaveStdOutContaining("error : Each child of '//postActions' should have a unique id"); string locFolder = Path.Combine(tmpDir, "content/TemplateWithSourceName/.template.config/localize"); diff --git a/test/Microsoft.TemplateEngine.TemplateLocalizer.Core.UnitTests/StringUpdaterTests.cs b/test/Microsoft.TemplateEngine.TemplateLocalizer.Core.UnitTests/StringUpdaterTests.cs index 31fcba325b8..cf66f2e50f8 100644 --- a/test/Microsoft.TemplateEngine.TemplateLocalizer.Core.UnitTests/StringUpdaterTests.cs +++ b/test/Microsoft.TemplateEngine.TemplateLocalizer.Core.UnitTests/StringUpdaterTests.cs @@ -148,6 +148,47 @@ await File.WriteAllTextAsync( Assert.DoesNotContain("existing translations in authoring language should be removed.", fileContent); } + [Fact] + public async Task UnchangedFileShouldntBeOverwritten() + { + CancellationTokenSource cts = new CancellationTokenSource(10000); + string expectedFilename = Path.Combine(_workingDirectory, "templatestrings.fr.json"); + + // Manually create a json to be observed. + JsonWriterOptions writerOptions = new JsonWriterOptions() + { + Encoder = new ExtendedJavascriptEncoder(), + Indented = true, + }; + using (FileStream fileStream = new FileStream(expectedFilename, FileMode.Create, FileAccess.Write)) + { + using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(fileStream, writerOptions); + + jsonWriter.WriteStartObject(); + + foreach (TemplateString locString in InputStrings) + { + jsonWriter.WritePropertyName(locString.LocalizationKey); + jsonWriter.WriteStringValue(locString.Value); + } + + jsonWriter.WriteEndObject(); + await jsonWriter.FlushAsync(cts.Token).ConfigureAwait(false); + } + + // Open the file and allow subsequent readings, but prevent writing. + // If something attempts to write to the file, it will get IOException. + using FileStream fileLock = new FileStream(expectedFilename, FileMode.Open, FileAccess.Read, FileShare.Read); + + // Attempt to update the previously created json to see if it will be overwritten. + // The content is identical. So we can read, but we shouldn't write to the file after this point. + await TemplateStringUpdater.UpdateStringsAsync(InputStrings, "en", new string[] { "fr" }, _workingDirectory, dryRun: false, NullLogger.Instance, cts.Token) + .ConfigureAwait(false); + + // An exception will be thrown, failing the test, if the call above tries to write to the file. + // The execution should reach this point if the call did not try to write to the file, which indicates success for the test. + } + [Fact] public async Task ExistingCommentsOfAuthoringLanguageArePreserved() { @@ -174,6 +215,40 @@ await File.WriteAllTextAsync( Assert.Contains("comments should be preserved.", fileContent); } + [Fact] + public async Task AllowedCharactersAreNotEscaped() + { + List locStrings = new List() + { + // No-break space shouldn't be escaped. + new TemplateString("..name", "name", "\u00A0") + }; + + CancellationTokenSource cts = new CancellationTokenSource(10000); + await TemplateStringUpdater.UpdateStringsAsync( + locStrings, + templateJsonLanguage: "en", + languages: new string[] { "it" }, + _workingDirectory, + dryRun: false, + NullLogger.Instance, + cts.Token) + .ConfigureAwait(false); + + string expectedFilename = Path.Combine(_workingDirectory, "templatestrings.it.json"); + string fileContent = File.ReadAllText(expectedFilename); + + Assert.Contains("\u00A0", fileContent); + Assert.DoesNotContain("\\u00A0", fileContent, StringComparison.OrdinalIgnoreCase); + + Dictionary resultStrings = + await ReadTemplateStringsFromJsonFile(expectedFilename, cts.Token) + .ConfigureAwait(false); + + Assert.Equal(locStrings.Count, resultStrings.Count); + Assert.All(locStrings, x => Assert.Contains(x.Value, resultStrings.Values)); + } + private static async Task> ReadTemplateStringsFromJsonFile(string path, CancellationToken cancellationToken) { using FileStream openStream = File.OpenRead(path); diff --git a/test/Microsoft.TemplateEngine.TemplateLocalizer.IntegrationTests/ExportCommandFailureTests.cs b/test/Microsoft.TemplateEngine.TemplateLocalizer.IntegrationTests/ExportCommandFailureTests.cs index dfba1ef14ae..026a535494c 100644 --- a/test/Microsoft.TemplateEngine.TemplateLocalizer.IntegrationTests/ExportCommandFailureTests.cs +++ b/test/Microsoft.TemplateEngine.TemplateLocalizer.IntegrationTests/ExportCommandFailureTests.cs @@ -48,7 +48,7 @@ public async Task PostActionsShouldHaveIds() .Should() .ExitWith(1) .And.HaveStdOutContaining("Generating localization files for a template.json has failed.") - .And.HaveStdOutContaining("Json element \"postActions/0\" must have a member \"id\"."); + .And.HaveStdOutContaining("Json element 'postActions/0' must have a member 'id'."); } [Fact] @@ -69,7 +69,7 @@ public async Task PostActionIdsAreUnique() .Should() .ExitWith(1) .And.HaveStdOutContaining("Generating localization files for a template.json has failed.") - .And.HaveStdOutContaining(@"Each child of ""//postActions"" should have a unique id. Currently, the id ""postActions/postAction1"" is shared by multiple children."); + .And.HaveStdOutContaining(@"Each child of '//postActions' should have a unique id. Currently, the id 'postActions/postAction1' is shared by multiple children."); } [Fact] @@ -117,7 +117,7 @@ public async Task MultipleManualInstructionShouldHaveIds() .Should() .ExitWith(1) .And.HaveStdOutContaining("Generating localization files for a template.json has failed.") - .And.HaveStdOutContaining(@"Json element ""manualInstructions/0"" must have a member ""id""."); + .And.HaveStdOutContaining("Json element 'manualInstructions/0' must have a member 'id'."); } [Fact] @@ -143,7 +143,7 @@ public async Task ManualInstructionIdsAreUnique() .Should() .ExitWith(1) .And.HaveStdOutContaining("Generating localization files for a template.json has failed.") - .And.HaveStdOutContaining("Each child of \"//postActions/0/manualInstructions\" should have a unique id. Currently, the id \"manualInstructions/mi\" is shared by multiple children."); + .And.HaveStdOutContaining("Each child of '//postActions/0/manualInstructions' should have a unique id. Currently, the id 'manualInstructions/mi' is shared by multiple children."); } private async Task CreateTemplateAndExport(string templateJsonContent)