Skip to content

Commit

Permalink
feat: Add support for attached properties localization
Browse files Browse the repository at this point in the history
  • Loading branch information
jeromelaban committed Sep 28, 2022
1 parent 2b2736d commit e726e39
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -613,12 +613,14 @@ private string[] GetResourceKeys(CancellationToken ct)
var doc = new XmlDocument();
doc.LoadXml(sourceText.ToString());
var rewriterBuilder = new StringBuilder();
//extract all localization keys from Win10 resource file
// https://docs.microsoft.com/en-us/dotnet/standard/data/xml/compiled-xpath-expressions?redirectedfrom=MSDN#higher-performance-xpath-expressions
// Per this documentation, /root/data should be more performant than //data
var keys = doc.SelectNodes("/root/data")
?.Cast<XmlElement>()
.Select(node => node.GetAttribute("name"))
.Select(node => RewriteResourceKeyName(rewriterBuilder, node.GetAttribute("name")))
.ToArray() ?? Array.Empty<string>();
_cachedResources[cachedFileKey] = new CachedResource(DateTimeOffset.Now, keys);
return keys;
Expand All @@ -642,7 +644,6 @@ private string[] GetResourceKeys(CancellationToken ct)
}
})
.Distinct()
.Select(k => k.Replace('.', '/'))
.ToArray();

#if DEBUG
Expand All @@ -651,6 +652,22 @@ private string[] GetResourceKeys(CancellationToken ct)
return resourceKeys;
}

private string RewriteResourceKeyName(StringBuilder builder, string keyName)
{
var firstDotIndex = keyName.IndexOf('.');
if (firstDotIndex != -1)
{
builder.Clear();
builder.Append(keyName);

builder[firstDotIndex] = '/';

return builder.ToString();
}

return keyName;
}

private DateTime GetLastBinaryUpdateTime()
{
// Determine the last update time, to allow for the re-generation of the files.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,44 @@ private IEnumerable<string> FindLocalizableProperties(XamlType xamlType)
type = type.BaseType;
}
}
private bool IsAttachedProperty(INamedTypeSymbol declaringType, string name)
=> _isAttachedProperty(declaringType, name);

private IEnumerable<(INamedTypeSymbol ownerType, string property)> FindLocalizableAttachedProperties(string uid)
{
foreach (var key in _resourceKeys.Where(k => k.StartsWith(uid + "/")))
{
// fullKey = $"{uidName}.[using:{ns}]{type}.{memberName}";
//
// Example:
// OpenVideosButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip

var firstDotIndex = key.IndexOf('/');

var propertyPath = key.Substring(firstDotIndex + 1);

const string usingPattern = "[using:";

if(propertyPath.StartsWith(usingPattern))
{
var lastDotIndex = propertyPath.LastIndexOf('.');

var propertyName = propertyPath.Substring(lastDotIndex + 1);
var typeName = propertyPath
.Substring(usingPattern.Length, lastDotIndex - usingPattern.Length)
.Replace("]", ".");

if(GetType(typeName) is { } typeSymbol)
{
yield return (typeSymbol, propertyName);
}
else
{
throw new Exception($"Unable to find the type {typeName} in key {key}");
}
}
}
}

private string[] FindLocalizableDeclaredProperties(INamedTypeSymbol type) => _findLocalizableDeclaredProperties!(type);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3135,7 +3135,7 @@ private void BuildExtendedProperties(IIndentedStringBuilder outerwriter, XamlObj
var isFrameworkElement = IsType(objectDefinitionType, _frameworkElementSymbol);
var hasIsParsing = HasIsParsing(objectDefinitionType);

if (extendedProperties.Any() || hasChildrenWithPhase || isFrameworkElement || hasIsParsing)
if (extendedProperties.Any() || hasChildrenWithPhase || isFrameworkElement || hasIsParsing || objectUid.HasValue())
{
string closureName;
if (!useGenericApply && objectDefinitionType is null)
Expand Down Expand Up @@ -3552,6 +3552,8 @@ private void BuildExtendedProperties(IIndentedStringBuilder outerwriter, XamlObj
BuildUiAutomationId(writer, closureName, uiAutomationId, objectDefinition);
}

BuildStatementLocalizedProperties(writer, objectDefinition, closureName);

if (hasIsParsing
// If true then this apply block will be applied to the content of a UserControl, which will already have had CreationComplete() called in its own apply block.
&& !useChildTypeForNamedElement
Expand Down Expand Up @@ -3807,7 +3809,7 @@ IMethodSymbol FindTargetMethodSymbol(INamedTypeSymbol? sourceType)
/// <summary>
/// Build localized properties which have not been set in the xaml.
/// </summary>
private void BuildLocalizedProperties(IIndentedStringBuilder writer, XamlObjectDefinition objectDefinition)
private void BuildInlineLocalizedProperties(IIndentedStringBuilder writer, XamlObjectDefinition objectDefinition)
{
TryAnnotateWithGeneratorSource(writer);
var objectUid = GetObjectUid(objectDefinition);
Expand All @@ -3827,6 +3829,28 @@ private void BuildLocalizedProperties(IIndentedStringBuilder writer, XamlObjectD
}
}

/// <summary>
/// Build localized properties which have not been set in the xaml.
/// </summary>
private void BuildStatementLocalizedProperties(IIndentedStringBuilder writer, XamlObjectDefinition objectDefinition, string closureName)
{
TryAnnotateWithGeneratorSource(writer);
var objectUid = GetObjectUid(objectDefinition);

if (objectUid != null)
{
var candidateAttachedProperties = FindLocalizableAttachedProperties(objectUid);
foreach (var candidate in candidateAttachedProperties)
{
var localizedValue = BuildLocalizedResourceValue(candidate.ownerType, candidate.property, objectUid);
if (localizedValue != null)
{
writer.AppendLineInvariantIndented($"{candidate.ownerType}.Set{candidate.property}({closureName}, {localizedValue});");
}
}
}
}

private void TryValidateContentPresenterBinding(IIndentedStringBuilder writer, XamlObjectDefinition objectDefinition, XamlMemberDefinition member)
{
TryAnnotateWithGeneratorSource(writer);
Expand Down Expand Up @@ -4637,7 +4661,7 @@ string Inner()
{
if (IsLocalizedString(propertyType, objectUid))
{
var resourceValue = BuildLocalizedResourceValue(owner, memberName, objectUid);
var resourceValue = BuildLocalizedResourceValue(FindType(owner?.Member.DeclaringType), memberName, objectUid);

if (resourceValue != null)
{
Expand Down Expand Up @@ -4855,7 +4879,7 @@ string Inner()
}
}

private string? BuildLocalizedResourceValue(XamlMemberDefinition? owner, string memberName, string objectUid)
private string? BuildLocalizedResourceValue(INamedTypeSymbol? owner, string memberName, string objectUid)
{
// see: https://docs.microsoft.com/en-us/windows/uwp/app-resources/localize-strings-ui-manifest
// Valid formats:
Expand Down Expand Up @@ -4888,13 +4912,12 @@ string Inner()
//windows 10 localization concat the xUid Value with the member value (Text, Content, Header etc...)
var fullKey = uidName + "/" + memberName;

if (owner != null && IsAttachedProperty(owner))
if (owner != null && IsAttachedProperty(owner, memberName))
{
var declaringType = GetType(owner.Member.DeclaringType);
var declaringType = owner;
var nsRaw = declaringType.ContainingNamespace.GetFullName();
var ns = nsRaw?.Replace(".", "/");
var type = declaringType.Name;
fullKey = $"{uidName}/[using:{ns}]{type}/{memberName}";
fullKey = $"{uidName}/[using:{nsRaw}]{type}.{memberName}";
}

if (_resourceKeys.Any(k => k == fullKey))
Expand Down Expand Up @@ -5790,7 +5813,7 @@ private void BuildChild(IIndentedStringBuilder writer, XamlMemberDefinition? own
RegisterAndBuildResources(writer, xamlObjectDefinition, isInInitializer: true);
BuildLiteralProperties(writer, xamlObjectDefinition);
BuildProperties(writer, xamlObjectDefinition);
BuildLocalizedProperties(writer, xamlObjectDefinition);
BuildInlineLocalizedProperties(writer, xamlObjectDefinition);
}

BuildExtendedProperties(writer, xamlObjectDefinition);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,32 @@ internal static void Write(string resourceMapName, string language, Dictionary<s
// "Magic" sequence to ensure we'll be reading a proper
// resource file at runtime
writer.Write(new byte[] { 0x75, 0x6E, 0x6F });
writer.Write(2); // version
writer.Write(3); // version

writer.Write(resourceMapName);
writer.Write(language);
writer.Write(resources.Count);

StringBuilder sb = new();

foreach (var pair in resources)
{
writer.Write(pair.Key.Replace(".", "/"));
var key = pair.Key;

var firstDotIndex = key.IndexOf('.');
if (firstDotIndex != -1)
{
sb.Clear();
sb.Append(key);

// Store the key as "uid/propertypath", while keeping the
// rest of the path with dots as needed (e.g. for attached properties)
sb[firstDotIndex] = '/';

key = sb.ToString();
}

writer.Write(key);
writer.Write(pair.Value);
}
}
Expand Down

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

11 changes: 7 additions & 4 deletions src/Uno.UI.Tests/ResourceLoader/Strings/en/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,13 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ApplicationName" xml:space="preserve">
<data name="ApplicationName" xml:space="preserve">
<value>App70-en</value>
</data>
<data name="Given_ResourceLoader.When_LocalizedResource" xml:space="preserve">
<data name="Given_ResourceLoader.When_LocalizedResource" xml:space="preserve">
<value>Text in 'en'</value>
</data>
<data name="ThemeRadioButtons.Header" xml:space="preserve">
<data name="ThemeRadioButtons.Header" xml:space="preserve">
<value>Header in 'en'</value>
</data>
<data name="TestEmptyConverterText.ValueIfNotNull" xml:space="preserve">
Expand All @@ -135,4 +135,7 @@
<data name="TestEmptyResource" xml:space="preserve">
<value />
</data>
</root>
<data name="OpenVideosButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Localized value</value>
</data>
</root>
8 changes: 7 additions & 1 deletion src/Uno.UI.Tests/Uno.UI.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<UnoForceHotReloadCodeGen>true</UnoForceHotReloadCodeGen>
</PropertyGroup>

<!--
Uncomment to troubleshoot source generation
<PropertyGroup>
<UnoUISourceGeneratorDebuggerBreak>True</UnoUISourceGeneratorDebuggerBreak>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
-->
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<UserControl x:Class="Uno.UI.Tests.Windows_UI_Xaml_Markup.XUidTests.Controls.When_XUid_And_AttachedProperty"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Uno.UI.Tests.Windows_UI_Xaml_Markup.XUidTests.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<StackPanel>
<Button x:Uid="OpenVideosButton"
x:Name="button2"
x:FieldModifier="public" />

<Button x:Uid="OpenVideosButton"
x:Name="button1"
x:FieldModifier="public"
ToolTipService.ToolTip="original string" />
</StackPanel>

</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

namespace Uno.UI.Tests.Windows_UI_Xaml_Markup.XUidTests.Controls
{
public sealed partial class When_XUid_And_AttachedProperty : UserControl
{
public When_XUid_And_AttachedProperty()
{
this.InitializeComponent();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,14 @@ public void When_EmptyXUid()
Assert.IsNull(conv.ValueIfNotNull);
Assert.AreEqual("Test", conv.ValueIfNull);
}

[TestMethod]
public void When_AttachedProperty()
{
var SUT = new When_XUid_And_AttachedProperty();

Assert.AreEqual("Localized value", ToolTipService.GetToolTip(SUT.button1));
Assert.AreEqual("Localized value", ToolTipService.GetToolTip(SUT.button2));
}
}
}
Loading

0 comments on commit e726e39

Please sign in to comment.