Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<char> input = new ReadOnlySpan<char>(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);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -126,28 +126,36 @@
<comment>{0} is any string from a json file. Such as "postActions", "myParameter", "author" etc.</comment>
</data>
<data name="stringExtractor_log_jsonKeyIsNotUnique" xml:space="preserve">
<value>Each child of "{0}" should have a unique id. Currently, the id "{1}" is shared by multiple children.</value>
<value>Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children.</value>
<comment>{0} is an identifier string similar to "postActions/0/manualInstructions/2/text"
{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc.</comment>
</data>
<data name="stringExtractor_log_jsonMemberIsMissing" xml:space="preserve">
<value>Json element "{0}" must have a member "{1}".</value>
<value>Json element '{0}' must have a member '{1}'.</value>
<comment>{0} and {1} are strings such as "postActions", "manualInstructions", "id" etc.</comment>
</data>
<data name="stringExtractor_log_skippingAlreadyAddedElement" xml:space="preserve">
<value>The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0}</value>
<comment>{0} is a string similar to "postActions/0/manualInstructions/2/text"</comment>
</data>
<data name="stringUpdater_log_dataIsUnchanged" xml:space="preserve">
<value>The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}'</value>
<comment>{0} is a file path.</comment>
</data>
<data name="stringUpdater_log_failedToReadLocFile" xml:space="preserve">
<value>"Failed to read the existing strings from "{0}"</value>
<value>Failed to read the existing strings from '{0}'</value>
<comment>{0} is a file path.</comment>
</data>
<data name="stringUpdater_log_loadingLocFile" xml:space="preserve">
<value>Loading existing localizations from file "{0}"</value>
<value>Loading existing localizations from file '{0}'</value>
<comment>{0} is a file path.</comment>
</data>
<data name="stringUpdater_log_localizedStringAlreadyExists" xml:space="preserve">
<value>The file already contains a localized string for key '{0}'. The old value will be preserved.</value>
<comment>{0} is a file path.</comment>
</data>
<data name="stringUpdater_log_openingTemplatesJson" xml:space="preserve">
<value>"Opening the following templatestrings.json file for writing: "{0}"</value>
<value>Opening the following templatestrings.json file for writing: '{0}'</value>
<comment>{0} is a file path.</comment>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<Description>The core API for Template Localizer tool.</Description>
<IsPackable>true</IsPackable>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnablePublicApiAnalyzer>true</EnablePublicApiAnalyzer>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand All @@ -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<string, string>? 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;
}
}
}
Loading