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
8 changes: 4 additions & 4 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="Microsoft.Build.Framework" Version="18.9.6" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="18.9.6" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" Condition="'$(MSBuildProjectName)' != 'ReswPlusUnitTests'" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" Condition="'$(MSBuildProjectName)' == 'ReswPlusUnitTests'" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.15" />
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.3.1" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2526"
Condition="'$(MSBuildProjectName)' == 'ReswPlusWinAppSDKSample' Or '$(MSBuildProjectName)' == 'ReswPlusWinAppSDKSampleExternalLibrary'" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2526" Condition="'$(MSBuildProjectName)' == 'ReswPlusWinAppSDKSample' Or '$(MSBuildProjectName)' == 'ReswPlusWinAppSDKSampleExternalLibrary'" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.4.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
</Project>
</Project>
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ Without it, such a project is reported as `RESWP0005` and no code is generated f

🗨 [How to inject generated resources](docs/Injectable-resource-providers.md)

### Pseudo-localization

🗨 [How to test localization before translations are available](docs/Pseudo-localization.md)

### Generator performance diagnostics

To include compiler-measured source-generator timings in detailed build output, enable:
Expand Down
1 change: 1 addition & 0 deletions ReswPlus.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<Platform Name="x86" />
</Configurations>
<Folder Name="/ReswPlus/">
<Project Path="src/ReswPlus.BuildTasks/ReswPlus.BuildTasks.csproj" />
<Project Path="src/ReswPlus.CommandLine/ReswPlusCmd.csproj" />
<Project Path="src/ReswPlus.Shared/ReswPlus.Shared.shproj" />
<Project Path="src/ReswPlus.SourceGenerator/ReswPlus.SourceGenerator.csproj" />
Expand Down
103 changes: 103 additions & 0 deletions docs/Pseudo-localization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Pseudo-localization

Pseudo-localization generates artificial translations from the project's default-language `.resw` files.
It makes localization problems visible before real translations are available:

- accented characters reveal hard-coded UI strings
- expanded values reveal clipped text and fixed-width layouts
- mirrored text reveals right-to-left layout assumptions
- visible boundary markers reveal unintended trimming or concatenation

The generated files are build artifacts under `obj`; ReswPlus never changes or adds files to the source
language folders.

## Enable an accented pseudo-language

Enable pseudo-localization with an MSBuild property:

```xml
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<ReswPlusPseudoLocalization>Accented</ReswPlusPseudoLocalization>
</PropertyGroup>
```

ReswPlus reads the `.resw` files under the project's `DefaultLanguage` folder and adds generated
`qps-ploc` resources to the PRI before Windows resource indexing. For that build, it removes the original
`.resw` items from PRI indexing and replaces the generated AppX manifest languages with the enabled
pseudo-language. The transformed intermediate resources are indexed as the package's neutral strings, which
avoids retaining a real default-language fallback. This makes the pseudo-localized resources the only strings
Windows can select. For example:

```text
Welcome, {0}!
```

becomes similar to:

```text
⟦Ŵëŀçømë, {0}! ~~~~~~~⟧
```

Composite-format placeholders, escaped braces, and XML-like markup inside values are preserved.
Resource names, comments, and `#Format` declarations are not transformed.

## Test right-to-left layout

Use `Mirrored` to generate the Windows `qps-plocm` pseudo-locale:

```xml
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<ReswPlusPseudoLocalization>Mirrored</ReswPlusPseudoLocalization>
</PropertyGroup>
```

| Mode | Windows language | Behavior |
| --- | --- | --- |
| `Accented` | `qps-ploc` | Accents characters, expands text, and adds boundary markers |
| `Mirrored` | `qps-plocm` | Applies the same transformation inside a right-to-left override |

## Select the pseudo-language

No application code or system-language change is required. ReswPlus accepts one pseudo-localization mode per
build, excludes the original `.resw` languages, and advertises only the selected pseudo-language in the
generated AppX manifest. Windows therefore selects it automatically when the application starts.

Build once with `Accented` and once with `Mirrored` when a test suite needs to exercise both modes.

### System-level availability

Nothing needs to be installed or enabled at the system level on Windows 10 version 1803 and newer,
including Windows 11. Windows includes the `qps-*` pseudo-locales in its National Language Support
APIs, but intentionally hides them from language enumeration. They are not display-language packs,
so they cannot be selected as the Windows display language through Settings.

On these Windows versions, registry edits do not make pseudo-locales appear in the system language
list. A ReswPlus pseudo-localization build needs no override because its real localized strings are
excluded and the selected pseudo-language is the only packaged string language.

Windows 10 version 1709 and older allowed pseudo-locales to be exposed for enumeration by adding
their LCIDs under `HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Nls\Locale`. ReswPlus supports
modern UWP and Windows App SDK targets, so this legacy system configuration is not required. See
[Using pseudo-locales for localizability testing](https://learn.microsoft.com/windows/win32/intl/using-pseudo-locales-for-localization-testing)
for the legacy registry values and NLS details.

## Configure text expansion

Accented and mirrored values expand by 30 percent by default. Adjust the percentage from 0 through 200:

```xml
<ReswPlusPseudoLocalizationExpansion>50</ReswPlusPseudoLocalizationExpansion>
```

## Use in CI

Pseudo-localization is disabled unless `ReswPlusPseudoLocalization` is set. Because enabling it replaces the
packaged string languages, keep it scoped to test builds. A CI job can enable it without changing the project:

```console
dotnet build -p:ReswPlusPseudoLocalization=Accented
```

This verifies that the generated pseudo-language remains packageable and that all source resources can be
parsed. UI automation can then launch the built application and check for clipping, overlap, untranslated
strings, and right-to-left regressions.
4 changes: 4 additions & 0 deletions nuget/ReswPlus.SourceGenerator.NugetPackage.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
<ItemGroup>
<!-- Include the built DLL as an analyzer -->
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs"/>
<!-- Include the build-time pseudo-localization task -->
<None Include="$(MSBuildThisFileDirectory)..\src\ReswPlus.BuildTasks\bin\$(Configuration)\netstandard2.0\ReswPlus.BuildTasks.dll"
Pack="true"
PackagePath="tools/netstandard2.0/" />
<!-- Package metadata -->
<None Include="$(MSBuildThisFileDirectory)Icon.png" Pack="true" PackagePath=""/>
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath=""/>
Expand Down
62 changes: 62 additions & 0 deletions nuget/ReswPlus.targets
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,74 @@
<ReswPlusGenerateResourceInterfaces Condition="'$(ReswPlusGenerateResourceInterfaces)' == ''">true</ReswPlusGenerateResourceInterfaces>
<!-- Ask the compiler to include source-generator timings in detailed build output. -->
<ReswPlusReportGeneratorPerformance Condition="'$(ReswPlusReportGeneratorPerformance)' == ''">false</ReswPlusReportGeneratorPerformance>
<!-- Generate intermediate qps-ploc/qps-plocm resources before PRI indexing. -->
<ReswPlusPseudoLocalization Condition="'$(ReswPlusPseudoLocalization)' == ''">None</ReswPlusPseudoLocalization>
<ReswPlusPseudoLocalizationExpansion Condition="'$(ReswPlusPseudoLocalizationExpansion)' == ''">30</ReswPlusPseudoLocalizationExpansion>
<_ReswPlusBuildTasksAssembly Condition="'$(_ReswPlusBuildTasksAssembly)' == '' And Exists('$(MSBuildThisFileDirectory)..\tools\netstandard2.0\ReswPlus.BuildTasks.dll')">$(MSBuildThisFileDirectory)..\tools\netstandard2.0\ReswPlus.BuildTasks.dll</_ReswPlusBuildTasksAssembly>
<_ReswPlusBuildTasksAssembly Condition="'$(_ReswPlusBuildTasksAssembly)' == '' And Exists('$(MSBuildThisFileDirectory)..\src\ReswPlus.BuildTasks\bin\$(Configuration)\netstandard2.0\ReswPlus.BuildTasks.dll')">$(MSBuildThisFileDirectory)..\src\ReswPlus.BuildTasks\bin\$(Configuration)\netstandard2.0\ReswPlus.BuildTasks.dll</_ReswPlusBuildTasksAssembly>
</PropertyGroup>
<UsingTask TaskName="ReswPlus.BuildTasks.GeneratePseudoResources"
AssemblyFile="$(_ReswPlusBuildTasksAssembly)"
Condition="'$(_ReswPlusBuildTasksAssembly)' != ''" />
<UsingTask TaskName="ReswPlus.BuildTasks.SetPseudoLanguagesInAppxManifest"
AssemblyFile="$(_ReswPlusBuildTasksAssembly)"
Condition="'$(_ReswPlusBuildTasksAssembly)' != ''" />
<Target Name="EnableReswPlusGeneratorPerformanceReport"
BeforeTargets="CoreCompile"
Condition="'$(ReswPlusReportGeneratorPerformance)' == 'true'">
<PropertyGroup>
<ReportAnalyzer>true</ReportAnalyzer>
</PropertyGroup>
</Target>
<Target Name="GenerateReswPlusPseudoResources"
BeforeTargets="_GenerateProjectPriConfigurationFiles;GenerateProjectPriFile"
Condition="'$(ReswPlusPseudoLocalization)' != 'None' And '$(ReswPlusPseudoLocalization)' != 'false'">
<Error Condition="'$(_ReswPlusBuildTasksAssembly)' == ''"
Text="ReswPlus pseudo-localization is enabled, but ReswPlus.BuildTasks.dll could not be found." />
<Error Condition="'$(DefaultLanguage)' == ''"
Text="ReswPlus pseudo-localization requires the project to declare DefaultLanguage." />
<GeneratePseudoResources Resources="@(PRIResource)"
DefaultLanguage="$(DefaultLanguage)"
ProjectDirectory="$(MSBuildProjectDirectory)"
IntermediateOutputPath="$(IntermediateOutputPath)"
Modes="$(ReswPlusPseudoLocalization)"
ExpansionPercentage="$(ReswPlusPseudoLocalizationExpansion)">
<Output TaskParameter="GeneratedResources" ItemName="_ReswPlusGeneratedPseudoResource" />
<Output TaskParameter="PseudoLanguage" PropertyName="_ReswPlusPseudoLanguage" />
</GeneratePseudoResources>
<PropertyGroup>
<DefaultLanguage>$(_ReswPlusPseudoLanguage)</DefaultLanguage>
<DefaultResourceLanguage>$(_ReswPlusPseudoLanguage)</DefaultResourceLanguage>
</PropertyGroup>
<ItemGroup>
<PRIResource Remove="@(PRIResource)"
Condition="'%(Extension)' == '.resw'" />
<PRIResource Include="@(_ReswPlusGeneratedPseudoResource)" />
<FileWrites Include="@(_ReswPlusGeneratedPseudoResource)" />
</ItemGroup>
<Message Importance="high"
Text="ReswPlus generated @(_ReswPlusGeneratedPseudoResource->Count()) pseudo-localized resource file(s)." />
</Target>
<Target Name="TrackReswPlusPseudoLanguagesForAppxManifest"
BeforeTargets="_GenerateCurrentProjectAppxManifest"
Condition="'$(FinalAppxManifestName)' != ''">
<PropertyGroup>
<_ReswPlusPseudoManifestInput>$(IntermediateOutputPath)ReswPlus\PseudoLocalization\manifest.mode</_ReswPlusPseudoManifestInput>
</PropertyGroup>
<MakeDir Directories="$([System.IO.Path]::GetDirectoryName('$(_ReswPlusPseudoManifestInput)'))" />
<WriteLinesToFile File="$(_ReswPlusPseudoManifestInput)"
Lines="$(ReswPlusPseudoLocalization)"
Overwrite="true"
WriteOnlyWhenDifferent="true" />
<ItemGroup>
<_GenerateCurrentProjectAppxManifestInput Include="$(_ReswPlusPseudoManifestInput)" />
<FileWrites Include="$(_ReswPlusPseudoManifestInput)" />
</ItemGroup>
</Target>
<Target Name="AddReswPlusPseudoLanguagesToAppxManifest"
AfterTargets="_GenerateCurrentProjectAppxManifest"
Condition="'$(ReswPlusPseudoLocalization)' != 'None' And '$(ReswPlusPseudoLocalization)' != 'false' And Exists('$(FinalAppxManifestName)')">
<SetPseudoLanguagesInAppxManifest ManifestPath="$(FinalAppxManifestName)"
Modes="$(ReswPlusPseudoLocalization)" />
</Target>
</Project>
Loading
Loading