Compare commits

6 Commits

Author SHA1 Message Date
5a13431603 add culture based caching 2023-05-20 15:22:18 +02:00
49e16a8077 add how to chapter 2023-05-20 10:43:47 +00:00
2075b35f7b fix resource class name generation -> 0.0.3 2023-05-19 18:43:41 +00:00
4c1f36e5f8 rm ilmerge, new version 0.0.2 2023-05-18 21:36:54 +00:00
b62e34eea2 add version 0.0.1 2023-05-18 17:04:31 +02:00
433e6a2727 add draft 2023-02-26 19:03:30 +01:00
40 changed files with 2512 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
"name": "C# (.NET)",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/dotnet:0-6.0",
"features": {
"ghcr.io/devcontainers/features/nix:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"formulahendry.dotnet-test-explorer",
"jnoortheen.nix-ide",
"eamodio.gitlens"
]
}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [5000, 5001],
// "portsAttributes": {
// "5001": {
// "protocol": "https"
// }
// }
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "nix-env -i nixpkgs-fmt"
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

478
.gitignore vendored Normal file
View File

@@ -0,0 +1,478 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
result

40
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,40 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": "Launch Example App",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/example/Example.App/bin/Debug/net6.0/Example.App.dll",
"args": [],
"cwd": "${workspaceFolder}/example/Example.App",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Update Resources.cs",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/ResourceString.Net.App.Console/bin/Debug/net6.0/ResourceString.Net.App.Console.dll",
"args": [
"${workspaceFolder}/ResourceString.Net.Logic/Properties/Resources.resx"
],
"cwd": "${workspaceFolder}/ResourceString.Net.App.Console",
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"dotnet-test-explorer.testProjectPath": "**/*Tests.csproj"
}

41
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "nix",
"type": "process",
"args": [
"--experimental-features",
"nix-command flakes",
"run",
".#devTasks.buildAllAssemblies"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/example/Example.App/Example.App.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/example/Example.App/Example.App.csproj"
],
"problemMatcher": "$msCompile"
}
]
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 stubbfel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

368
Readme.md
View File

@@ -0,0 +1,368 @@
# ResourceString.Net
## What is ResourceString.Net?
ResourceString.Net is a powerful .NET library that allows you to work with string resources in a type-safe manner.
It leverages the `resx` file in your project and utilizes a [c# source code generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) to create a comprehensive API. No [`Designer.cs`](https://learn.microsoft.com/en-us/dotnet/framework/tools/resgen-exe-resource-file-generator?source=recommendations) or [`T4 Text Templates`](https://learn.microsoft.com/en-us/visualstudio/modeling/code-generation-and-t4-text-templates?) filer for resources are required any more.
With ResourceString.Net, you can handle resource strings as "multi-language strings" (see [The ResourceString-Classes](#the-resourcestring-classes)) instead of built-in strings.
This provides the ability to switch languages during runtime without the need to rerun string factory methods.
Additionally, ResourceString.Net ensures that formatted strings have methods with the correct number of expected parameters.
## Installation and Setup Instructions
To incorporate ResourceString.Net into your .NET application, follow these simple steps:
1. Install the NuGet package by executing the following command in the NuGet package manager or the dotnet CLI:
```sh
dotnet add package ResourceString.Net
```
2. Once the package is installed, you can start using the APIs in your application.
## Getting Started
To quickly get started with ResourceString.Net, follow the steps below:
1. Run the following script to create a new project and a resource file:
```sh
dotnet new console --name MyTestConsoleApp
cd MyTestConsoleApp
dotnet add package "System.Resources.Extensions"
dotnet add package "ResourceString.Net"
echo "<?xml version='1.0' encoding='utf-8'?>
<root>
<data name='Greetings'>
<value>Hello {0}</value>
<comment>0 = name</comment>
</data>
<data name='World'>
<value>World</value>
</data>
</root>
" > Resources.resx
echo "var message = MyTestConsoleApp.Resources.Greetings.From(
MyTestConsoleApp.Resources.World
);
Console.WriteLine(message.Value);
" > Program.cs
```
2. Add the following `PropertyGroup` to the `MyTestConsoleApp.csproj` file to enable ResourceString.Net to handle the project's resource file:
```xml
<PropertyGroup>
<AdditionalFileItemNames>$(AdditionalFileItemNames);EmbeddedResource</AdditionalFileItemNames>
</PropertyGroup>
```
3. Run the project using the following command:
```sh
dotnet run
# Expected output: Hello World
```
During compile time, the ResourceString.Net source code generator will automatically add the following code to the `MyTestConsoleApp.csproj` project:
```cs
using ResourceString.Net.Contract;
using System;
using System.Globalization;
using System.Resources;
using System.Threading;
namespace MyTestConsoleApp
{
internal static class Resources
{
#region ResourceManager
private static readonly Type _Type = typeof(Resources);
private static readonly Lazy<ResourceManager> _ResourceManager = new Lazy<ResourceManager>(
() => new ResourceManager("MyTestConsoleApp.Resources" ?? string.Empty, _Type.Assembly),
LazyThreadSafetyMode.PublicationOnly
);
public static ResourceManager ResourceManager => _ResourceManager.Value;
private static CultureInfo GetDefaultCulture()
{
return CultureInfo.CurrentCulture;
}
private static IResourceString AddToCultureCache(IResourceString source)
{
return new CultureBasedCachedString(source, GetDefaultCulture);
}
#endregion // ResourceManager
#region Greetings
internal static class Greetings
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString("Greetings", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString name) => AddToCultureCache(new FormattedResourceString(
Format,
GetDefaultCulture,
name
));
}
#endregion // Greetings
#region World
private static readonly Lazy<IResourceString> LazyWorld = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString("World", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString World => LazyWorld.Value;
#endregion // World
}
}
```
### Add Multi-Languages
Run the following script to add a language specific resource file and add a culture change during runtime:
```sh
echo "<?xml version='1.0' encoding='utf-8'?>
<root>
<data name='Greetings'>
<value>Hallo {0}</value>
<comment>0 = name</comment>
</data>
<data name='World'>
<value>Welt</value>
</data>
</root>
" > Resources.de.resx
echo "
Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo(\"de-DE\");
Console.WriteLine(message.Value);
" >> Program.cs
dotnet run
# Expected output: Hello World \n Hallo Welt
```
In the code from `Program.cs` you can see there is explicit rebuild of the format string required after the culture switch.
The `IResourceString` object themselves are rebuilding the culture specific string.
## How it works
The ResourceString.Net source code generator operates by reading all `AdditionalFiles` with the `.resx` file extension and generating a static class based on the specified `data` elements within the file.
For example, given the following XML element:
```xml
<data name='World'>
<value>Welt</value>
</data>
```
the source code generator transforms it into the following `C#` class members:
```cs
#region World
private static readonly Lazy<IResourceString> LazyWorld = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString("World", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString World => LazyWorld.Value;
#endregion // World
```
If an element contains a format string, such as:
```xml
<data name='Greetings'>
<value>Hello {0} and {1}</value>
</data>
```
the generator generates following code to support the formatted string:
```cs
#region Greetings
internal static class Greetings
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString("Greetings", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString p1, IResourceString p2) => AddToCultureCache(new FormattedResourceString(
Format,
GetDefaultCulture,
p1,
p2
));
}
#endregion // Greetings
```
In cases where the element with the format string includes a comment element like:
```xml
<data name='Greetings'>
<value>Hello {0} and {1}</value>
<comment>0 = name, 1 = otherName </comment>
</data>
```
the source generator extracts the parameter names from the comment instead of using generic names:
```cs
public static IResourceString From(IResourceString name, IResourceString otherName) => AddToCultureCache(new FormattedResourceString(
Format,
name,
otherName
));
```
This allows for more descriptive parameter names in the generated code.
## The ResourceString-Classes
### IResourceString
- Interface that defines the contract for resource strings.
- Contains properties: `Value`, representing the string value, and `GetValue(CultureInfo cultureInfo)`, for retrieving the value for a specific `CultureInfo`.
### FormattedResourceString
- Implements the `IResourceString` interface.
- Represents a resource string with placeholders for parameters.
- Allows for dynamic formatting of the string by providing a format and an array of parameters.
- Formats the string by replacing the placeholders with the parameter values.
### JoinedResourceString
- Implements the `IResourceString` interface.
- Represents a resource string that joins multiple elements with a separator.
- Useful for constructing strings that involve concatenating multiple resource strings or literal strings together.
- Allows customization of the separator between the elements.
### LiteralString
- Implements the `IResourceString` interface.
- Represents a literal string resource.
- Provides the actual string value as-is without any formatting or localization.
- Can be used for static, non-localized strings.
### ResourceManagerString
- Implements the `IResourceString` interface.
- Represents a resource string retrieved from a `ResourceManager`.
- Provides access to resource strings stored in `resx` files or other resource sources.
- Handles retrieving the localized value for the specified `CultureInfo`.
### CultureBasedCachedString
- Implements the `IResourceString` interface.
- Enhances performance by avoiding redundant resource string lookups and reducing the overhead associated with repeated string generation.
## Console App: ResourceString.Net.App.Console
The ResourceString.Net project also provide a console app, `ResourceString.Net.App.Console`, which allows you to generate a C# class based on a given resource file. This can be useful for automating the creation of resource classes in your projects without usage of the `source generator`.
### Usage
To use the console app, follow these steps:
1. Build the console app from this source repository
2. Open a command prompt or terminal.
3. Navigate to the directory where the `ResourceString.Net.App.Console` executable is located.
#### Syntax
```
ResourceString.Net.App.Console <sourceFile> [namespaceString] [className]
```
#### Parameters
- `<sourceFile>`: Required. The path to the resource file (e.g., `Resources.resx`) that you want to generate the C# class from.
- `[namespaceString]` (optional): The namespace for the generated C# class. If not provided, the default value is `"Properties"`.
- `[className]` (optional): The name of the generated C# class. If not provided, the default value is `"Resources"`.
#### Examples
Here are some examples of how to use the console app:
- Generate a C# class named `Resources.cs` in the `"Properties"` namespace based on the `Resources.resx` file:
```
ResourceString.Net.App.Console Resources.resx
```
- Generate a C# class named `MyResources.cs` in the `"MyNamespace"` namespace based on the `MyResources.resx` file:
```
ResourceString.Net.App.Console MyResources.resx MyNamespace MyResources
```
### Output
The console app will generate the C# class code based on the provided resource file and output it to the console.
You can redirect the output to a file if desired.
## Third party packages
| Package | Version |
| --------------------------------------------------------------------------------------------------- | ------- |
| [Microsoft.CodeAnalysis.Analyzers](https://www.nuget.org/packages/Microsoft.CodeAnalysis.Analyzers) | 3.3.4 |
| [Microsoft.CodeAnalysis.CSharp](https://www.nuget.org/packages/Microsoft.CodeAnalysis.CSharp) | 4.3.0 |
| [NETStandard.Library](https://www.nuget.org/packages/NETStandard.Library) | 2.0.3 |
| [LanguageExt.Core](https://www.nuget.org/packages/LanguageExt.Core) | 4.4.3 |
| [System.Resources.Extensions](https://www.nuget.org/packages/System.Resources.Extensions) | 7.0.0 |
## Development Notes
Here are some useful aliases for running Nix commands with experimental features:
- `nixe`: This alias runs the `nix` command with the experimental feature flag `nix-command flakes`.
- `nulock`: This alias runs the `nixe` command with the argument `run .#devTasks.updateNugetLock`, which updates the NuGet lock file.
- `fllock`: This alias runs the `nixe` command with the argument `run .#devTasks.updateFlakeLock`, which updates the Flake lock file.
- `ulock`: This alias combines the `nulock` and `fllock` aliases to update both the NuGet and Flake lock files.
To load the `alias.sh` source file from the current folder, use the following command:
```sh
source ./alias.sh
```
By executing the command above, you'll make the aliases available for use in the current terminal session.

View File

@@ -0,0 +1,25 @@
using ResourceString.Net.Logic.Factories;
using ResourceString.Net.Logic.Parsers.Resx;
var sourceFile = args.First();
var namespaceString = args.Skip(1).FirstOrDefault() ?? "Properties";
var resourceFileName = args.Skip(2).FirstOrDefault() ?? "Resources";
var className = CodeSnippetFactory.TransformToClassName(
resourceFileName
);
var result = Parser.TryParse(System.IO.File.ReadAllText(sourceFile)).Match(
Some: v => CodeSnippetFactory.CreateResourceClassCodeSnippet(
CodeSnippetFactory.TransformToNamespace(namespaceString),
className,
CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(
$"{namespaceString}.{className}",
className
),
v.Resources
),
None: () => throw new InvalidOperationException()
);
Console.WriteLine(result.Value.Trim());

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<PackAsTool>true</PackAsTool>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Deterministic>true</Deterministic>
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../ResourceString.Net.Logic/ResourceString.Net.Logic.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Globalization;
namespace ResourceString.Net.Contract
{
public sealed class CultureBasedCachedString : IResourceString
{
private readonly Func<CultureInfo> m_GetDefaultCulture;
private readonly IResourceString m_ResourceString;
private readonly Dictionary<CultureInfo, string> m_Cache = new Dictionary<CultureInfo, string>();
public CultureBasedCachedString(
IResourceString resourceString,
Func<CultureInfo>? getDefaultCulture = default)
{
m_ResourceString = resourceString;
m_GetDefaultCulture = getDefaultCulture ?? (() => CultureInfo.CurrentCulture);
}
public string Value => GetValue(m_GetDefaultCulture());
public string GetValue(CultureInfo cultureInfo)
{
if (m_Cache.TryGetValue(cultureInfo, out var value))
{
return value;
}
var newValue = m_ResourceString.GetValue(cultureInfo);
m_Cache[cultureInfo] = newValue;
return newValue;
}
}
}

View File

@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System;
namespace ResourceString.Net.Contract
{
public sealed class FormattedResourceString : IResourceString
{
private readonly Func<CultureInfo> m_GetDefaultCulture;
public IResourceString Format { get; }
public IEnumerable<IResourceString> Parameters { get; }
public string Value => GetValue(CultureInfo.CurrentCulture);
public FormattedResourceString(
IResourceString format,
Func<CultureInfo>? getDefaultCulture = default,
params IResourceString[] parameters)
{
Format = format ?? new JoinedResourceString(
LiteralString.Factory(","),
getDefaultCulture,
parameters.Select(((p, idx) => LiteralString.Factory("{" + idx + "}"))).ToArray()
);
Parameters = parameters;
m_GetDefaultCulture = getDefaultCulture ?? (() => CultureInfo.CurrentCulture);
}
public string GetValue(CultureInfo cultureInfo)
{
var format = Format.GetValue(cultureInfo);
var parameterStrings = Parameters.Select(p => p.GetValue(cultureInfo));
return string.Format(format, parameterStrings.ToArray());
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Globalization;
namespace ResourceString.Net.Contract
{
public interface IResourceString
{
string Value { get; }
string GetValue(CultureInfo cultureInfo);
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System;
namespace ResourceString.Net.Contract
{
public sealed class JoinedResourceString : IResourceString
{
private readonly Func<CultureInfo> m_GetDefaultCulture;
public IResourceString Separator { get; }
public IEnumerable<IResourceString> Elements { get; }
public string Value => GetValue(CultureInfo.CurrentCulture);
public JoinedResourceString(
IResourceString separator,
Func<CultureInfo>? getDefaultCulture = default,
params IResourceString[] elements)
{
Separator = separator ?? new LiteralString(
() => ","
);
Elements = elements;
m_GetDefaultCulture = getDefaultCulture ?? (() => CultureInfo.CurrentCulture);
}
public string GetValue(CultureInfo cultureInfo)
{
var separator = Separator.GetValue(cultureInfo);
var elementsStrings = Elements.Select(p => p.GetValue(cultureInfo));
return string.Join(separator, elementsStrings);
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Globalization;
using System.Threading;
namespace ResourceString.Net.Contract
{
public sealed class LiteralString : IResourceString
{
private Lazy<string> m_Value;
public string Value => m_Value.Value;
public static IResourceString Empty { get; } = Factory(string.Empty);
public static IResourceString Factory(string source)
{
return new LiteralString(() => source);
}
public LiteralString(Func<string> factory)
{
m_Value = new Lazy<string>(
() => factory?.Invoke() ?? string.Empty,
LazyThreadSafetyMode.PublicationOnly
);
}
public string GetValue(CultureInfo _) => Value;
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Globalization;
using System.Resources;
namespace ResourceString.Net.Contract
{
public class ResourceManagerString : IResourceString
{
private readonly Func<CultureInfo> m_GetDefaultCulture;
public string Id { get; }
public ResourceManager Manager { get; }
public string Value => GetValue(m_GetDefaultCulture());
public ResourceManagerString(
string id,
ResourceManager manager,
Func<CultureInfo>? getDefaultCulture = default)
{
Id = id ?? string.Empty;
Manager = manager ?? throw new ArgumentNullException(nameof(manager));
m_GetDefaultCulture = getDefaultCulture ?? (() => CultureInfo.CurrentCulture);
}
public string GetValue(CultureInfo cultureInfo)
{
return Manager.GetString(Id, cultureInfo);
}
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Deterministic>true</Deterministic>
<Optimize>true</Optimize>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,112 @@
using ResourceString.Net.Logic.Factories;
namespace ResourceString.Net.Logic.Tests;
[TestClass]
public class LogicResourcesTests
{
[TestMethod]
public void Test_Get_ResourceStringMembers()
{
var rm = Properties.Resources.ResourceManager;
Assert.AreEqual(
new FormattedResourceString(
Properties.Resources.ResourceStringMembers.Format,
default,
LiteralString.Factory("ResourceStringMembers"),
LiteralString.Factory("ResourceManager")
).Value,
string.Format(
rm.GetString("ResourceStringMembers") ?? string.Empty,
"ResourceStringMembers",
"ResourceManager"
)
);
}
[TestMethod]
public void Test_Get_ResourceFormatClassMembers()
{
var rm = Properties.Resources.ResourceManager;
Assert.AreEqual(
new FormattedResourceString(
Properties.Resources.ResourceFormatClassMembers.Format,
default,
LiteralString.Factory("ResourceFormatClassMembers"),
LiteralString.Factory("ResourceManager"),
LiteralString.Factory("Format")
).Value,
string.Format(
rm.GetString("ResourceFormatClassMembers") ?? string.Empty,
"ResourceFormatClassMembers",
"ResourceManager",
"Format"
)
);
}
[TestMethod]
public void Test_Get_ResourceFormatClassFromMethod()
{
var rm = Properties.Resources.ResourceManager;
Assert.AreEqual(
new FormattedResourceString(
Properties.Resources.ResourceFormatClassFromMethod.Format,
default,
LiteralString.Factory("IResourceString name"),
LiteralString.Factory("name")
).Value,
string.Format(
rm.GetString("ResourceFormatClassFromMethod") ?? string.Empty,
"IResourceString name",
"name"
)
);
}
[TestMethod]
public void Test_Get_ResourceManagerMemberTemplate()
{
var rm = Properties.Resources.ResourceManager;
Assert.AreEqual(
CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(
"Resources",
"Resources"
).Value,
string.Format(
rm.GetString("ResourceManagerMemberTemplate") ?? string.Empty,
"Resources",
"Resources"
)
);
}
[TestMethod]
public void Test_Get_ResourcesClassTemplate()
{
var rm = Properties.Resources.ResourceManager;
var rmSnippet = CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(
"Resources",
"Resources"
);
Assert.AreEqual(
CodeSnippetFactory.CreateResourceClassCodeSnippet(
"ResourceString.Net.Logic.Properties",
"Resources",
rmSnippet,
Enumerable.Empty<IResourceString>()
).Value,
string.Format(
rm.GetString("ResourcesClassTemplate") ?? string.Empty,
"ResourceString.Net.Logic.Properties",
"Resources",
rmSnippet.Value,
string.Empty
)
);
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../ResourceString.Net.Logic/ResourceString.Net.Logic.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,203 @@
using ResourceString.Net.Logic.DomainObjects.Resx;
using ResourceString.Net.Logic.Factories;
using ResourceString.Net.Logic.Parsers.Resx;
namespace ResourceString.Net.Logic.Tests;
[TestClass]
public class ResxFileTests
{
private readonly string validXml = @$"<?xml version='1.0' encoding='utf-8'?>
<root>
<data name='Test1'>
<value>Value1</value>
<comment>This is a greeting message.</comment>
</data>
<data name='Test2' type='{typeof(string).FullName}'>
<value>{{2}}Value2{{0}}{{{{1}}}}</value>
<comment>2 = prefix</comment>
</data>
<data name='Test3' type='{typeof(byte[]).FullName}'>
<value>0xDEADBEEF</value>
</data>
</root>";
private const string expectedClass = @"
using ResourceString.Net.Contract;
using System;
using System.Globalization;
using System.Resources;
using System.Threading;
namespace TestNameSpace
{
internal static class TestResourceClass
{
#region ResourceManager
private static readonly Type _Type = typeof(TestResourceClass);
private static readonly Lazy<ResourceManager> _ResourceManager = new Lazy<ResourceManager>(
() => new ResourceManager(""ResourceManager"" ?? string.Empty, _Type.Assembly),
LazyThreadSafetyMode.PublicationOnly
);
public static ResourceManager ResourceManager => _ResourceManager.Value;
private static CultureInfo GetDefaultCulture()
{
return CultureInfo.CurrentCulture;
}
private static IResourceString AddToCultureCache(IResourceString source)
{
return new CultureBasedCachedString(source, GetDefaultCulture);
}
#endregion // ResourceManager
#region Test1
private static readonly Lazy<IResourceString> LazyTest1 = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString(""Test1"", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Test1 => LazyTest1.Value;
#endregion // Test1
internal static class Test2
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString(""Test2"", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString prefix, IResourceString p1) => AddToCultureCache(new FormattedResourceString(
Format,
GetDefaultCulture,
prefix,
p1
));
}
}
}";
private readonly string emptyXml = @"<?xml version='1.0' encoding='utf-8'?><root></root>";
private readonly string invalidXml = @"<?xml version='1.0' encoding='utf-8'?><root></data>";
[TestMethod]
public void Test_TryParse_ValidXml()
{
// Arrange
var expectedData = new[] {
("Test1", "Value1", string.Empty, "This is a greeting message."),
("Test2", "{2}Value2{0}{{1}}", typeof(string).FullName, "2 = prefix"),
("Test3", "0xDEADBEEF", typeof(byte[]).FullName, string.Empty)
};
// Act
var result = Parser.TryParse(validXml).Match(
Some: v => v.Resources,
None: () => throw new InvalidOperationException()
);
// Assert
CollectionAssert.AreEqual(
expectedData,
result.Select(i => (
i.Name,
i.Value,
i.Type.IfNone(string.Empty),
i.Comment.IfNone(string.Empty)
)).ToList()
);
}
[TestMethod]
public void Test_TryParse_EmptyXml()
{
// Arrange
var expectedData = Enumerable.Empty<Resource>();
// Act
var isSome = Parser.TryParse(emptyXml).Match(
Some: _ => true,
None: () => false
);
// Assert
Assert.IsFalse(isSome);
}
[TestMethod]
public void Test_TryParse_InValidXml()
{
// Arrange
var expectedData = Enumerable.Empty<Resource>();
// Act
var isSome = Parser.TryParse(invalidXml).Match(
Some: _ => true,
None: () => false
);
// Assert
Assert.IsFalse(isSome);
}
[TestMethod]
public void Test_ToMemberString()
{
// Act
var result = Parser.TryParse(validXml).Match(
Some: v => CodeSnippetFactory.CreateMemberCodeSnippets(v.Resources),
None: () => throw new InvalidOperationException()
);
// Assert
Assert.AreEqual(2, result.Count());
Assert.AreEqual(
"#region Test1",
result.Select(i => i.Value.Substring(0, 25).Trim()).ToList()[0]
);
Assert.AreEqual(
"internal static class Test2",
result.Select(i => i.Value.Substring(0, 45).Trim()).ToList()[1]
);
}
[TestMethod]
public void Test_ToClassString()
{
// Act
var result = Parser.TryParse(validXml).Match(
Some: v => CodeSnippetFactory.CreateResourceClassCodeSnippet(
"TestNameSpace",
"TestResourceClass",
CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet("ResourceManager", "TestResourceClass"),
v.Resources
),
None: () => throw new InvalidOperationException()
);
// Assert
Assert.AreEqual(
expectedClass.Trim(),
result.Value.Trim()
);
}
}

View File

@@ -0,0 +1,4 @@
global using LanguageExt;
global using Microsoft.VisualStudio.TestTools.UnitTesting;
global using ResourceString.Net.Contract;
global using ResourceString.Net.Logic;

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Linq;
namespace ResourceString.Net.Logic.DomainObjects.Resx;
internal record File
{
public File(IEnumerable<Resource> resources)
{
Resources = new Seq<Resource>(
resources?.Where(o => o != null) ?? Enumerable.Empty<Resource>()
);
}
public IEnumerable<Resource> Resources { get; }
}

View File

@@ -0,0 +1,18 @@
namespace ResourceString.Net.Logic.DomainObjects.Resx;
internal record Resource
{
public Resource(string name, string value)
{
Name = name ?? string.Empty;
Value = value ?? string.Empty;
}
public string Name { get;}
public Option<string> Type { get; init;}
public Option<string> Comment { get; init;}
public string Value { get; }
}

View File

@@ -0,0 +1,152 @@
using ResourceString.Net.Logic.DomainObjects.Resx;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace ResourceString.Net.Logic.Factories;
internal static class CodeSnippetFactory
{
public static IResourceString CreateResourceMangerMemberCodeSnippet(
string resourceName,
string className)
{
return Properties.Resources.ResourceManagerMemberTemplate.From(
LiteralString.Factory(resourceName),
LiteralString.Factory(className)
);
}
public static IEnumerable<IResourceString> CreateMemberCodeSnippets(IEnumerable<Resource> resources)
{
return CreateMemberCodeSnippets(
resources,
Properties.Resources.DefaultPropertyName_ResourceManager
);
}
public static IEnumerable<IResourceString> CreateMemberCodeSnippets(
IEnumerable<Resource> resources,
IResourceString resourceManagerName)
{
if (resources is null)
{
return Enumerable.Empty<IResourceString>();
}
var stringResources = resources.Where(r => r.Type.Match(
v => typeof(string).IsAssignableFrom(Type.GetType(v.Trim(), false, true)),
() => true
));
return stringResources.Select(r =>
{
var openBraces = r.Value
.Replace("{{", string.Empty)
.Replace("{{", string.Empty)
.Split('{')
.Skip(1)
.ToArray();
if (openBraces.Any())
{
var formatNames = new System.Collections.Generic.HashSet<string>(
openBraces.Select(b => b.Split('}').First())
);
var resolveParameterNames = r.Comment.Match(
c => c.Split(',')
.Select(str => str.Split('='))
.Where(parts => parts.Count() == 2)
.Select(parts => (
parts.First().Trim().Trim('{', '}').Split(' ').First(),
parts.Last().Trim().Split(' ').First()
)).Where(t => uint.TryParse(t.Item1, out var _))
.GroupBy(t => t.Item1)
.ToDictionary(k => k.Key, v => v.First().Item2),
() => new Dictionary<string, string>()
);
var parameterNames = formatNames.Select(
n => resolveParameterNames.TryGetValue(n.Trim(), out var paramName)
? paramName.Substring(0, 1).ToLower() + paramName.Substring(1)
: uint.TryParse(n, out var value) ? $"p{value + 1}" : n
).ToArray();
var from = Properties.Resources.ResourceFormatClassFromMethod.From(
new JoinedResourceString(
LiteralString.Factory(", "),
elements: parameterNames.Select(n => LiteralString.Factory($"{nameof(IResourceString)} {n}")).ToArray()
),
new JoinedResourceString(
LiteralString.Factory($",{System.Environment.NewLine} "),
elements: parameterNames.Select(n => LiteralString.Factory($"{n}")).ToArray()
)
);
return Properties.Resources.ResourceFormatClassMembers.From(
LiteralString.Factory(r.Name), resourceManagerName, from
);
}
return Properties.Resources.ResourceStringMembers.From(
LiteralString.Factory(r.Name), resourceManagerName
);
});
}
public static IResourceString CreateResourceClassCodeSnippet(
string namespaceString,
string resourceClassName,
IResourceString resourceManagerSnippet,
IEnumerable<IResourceString> memberSnippets)
{
return Properties.Resources.ResourcesClassTemplate.From(
LiteralString.Factory(namespaceString),
LiteralString.Factory(resourceClassName),
resourceManagerSnippet,
new JoinedResourceString(
LiteralString.Empty,
elements: memberSnippets?.ToArray() ?? Array.Empty<IResourceString>()
)
);
}
public static IResourceString CreateResourceClassCodeSnippet(
string namespaceString,
string resourceClassName,
IResourceString resourceManagerSnippet,
IEnumerable<Resource> resources
)
{
return CreateResourceClassCodeSnippet(
namespaceString,
resourceClassName,
resourceManagerSnippet,
CreateMemberCodeSnippets(resources)
);
}
public static string TransformToClassName(string input)
{
// Remove invalid characters and replace them with underscores
string sanitizedInput = Regex.Replace(
input?.Trim() ?? string.Empty,
@"[^a-zA-Z0-9_]", "_"
);
// Remove leading digits if the string starts with a digit
return Regex.Replace(sanitizedInput, @"^\d+", "");
}
public static string TransformToNamespace(string input)
{
// Remove invalid characters and replace them with dots
string sanitizedInput = Regex.Replace(
input?.Trim() ?? string.Empty,
@"[^a-zA-Z0-9_.]", "."
);
// Remove leading dots if present
return sanitizedInput.TrimStart('.');
}
}

View File

@@ -0,0 +1,3 @@
namespace System.Runtime.CompilerServices;
internal static class IsExternalInit { }

View File

@@ -0,0 +1,44 @@
using ResourceString.Net.Logic.DomainObjects.Resx;
using System.Linq;
using System.Xml;
namespace ResourceString.Net.Logic.Parsers.Resx;
internal static class Parser
{
public static Option<File> TryParse(string xmlString)
{
var doc = new XmlDocument();
try
{
doc.LoadXml(xmlString);
}
catch
{
return Option<File>.None;
}
var resources = doc.SelectNodes("descendant::data").OfType<XmlElement>().Select((i, _) =>
{
var name = i.GetAttribute("name");
var type = i.GetAttribute("type");
var comment = i.SelectSingleNode("descendant::comment")?.InnerXml ?? string.Empty;
var value = i.SelectSingleNode("descendant::value")?.InnerXml ?? string.Empty;
return new Resource(name, value)
{
Type = string.IsNullOrWhiteSpace(type)
? Option<string>.None
: Option<string>.Some(type),
Comment = string.IsNullOrWhiteSpace(comment)
? Option<string>.None
: Option<string>.Some(comment)
};
}).ToArray();
return resources.Any()
? Option<File>.Some(new File(resources))
: Option<File>.None;
}
}

View File

@@ -0,0 +1,141 @@
using ResourceString.Net.Contract;
using System;
using System.Globalization;
using System.Resources;
using System.Threading;
namespace ResourceString.Net.Logic.Properties
{
internal static class Resources
{
#region ResourceManager
private static readonly Type _Type = typeof(Resources);
private static readonly Lazy<ResourceManager> _ResourceManager = new Lazy<ResourceManager>(
() => new ResourceManager("ResourceString.Net.Logic.Properties.Resources" ?? string.Empty, _Type.Assembly),
LazyThreadSafetyMode.PublicationOnly
);
public static ResourceManager ResourceManager => _ResourceManager.Value;
private static CultureInfo GetDefaultCulture()
{
return CultureInfo.CurrentCulture;
}
private static IResourceString AddToCultureCache(IResourceString source)
{
return new CultureBasedCachedString(source, GetDefaultCulture);
}
#endregion // ResourceManager
internal static class ResourceStringMembers
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString("ResourceStringMembers", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString resourceId, IResourceString resourceManagerPropertyName) => AddToCultureCache(new FormattedResourceString(
Format,
GetDefaultCulture,
resourceId,
resourceManagerPropertyName
));
}
internal static class ResourceFormatClassMembers
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString("ResourceFormatClassMembers", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString resourceId, IResourceString resourceManagerPropertyName, IResourceString fromMethodDefinition) => AddToCultureCache(new FormattedResourceString(
Format,
GetDefaultCulture,
resourceId,
resourceManagerPropertyName,
fromMethodDefinition
));
}
internal static class ResourceFormatClassFromMethod
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString("ResourceFormatClassFromMethod", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString fromMethodSignature, IResourceString parameterNames) => AddToCultureCache(new FormattedResourceString(
Format,
GetDefaultCulture,
fromMethodSignature,
parameterNames
));
}
internal static class ResourcesClassTemplate
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString("ResourcesClassTemplate", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString ns, IResourceString className, IResourceString resourceManagerRegion, IResourceString resourceRegions) => AddToCultureCache(new FormattedResourceString(
Format,
GetDefaultCulture,
ns,
className,
resourceManagerRegion,
resourceRegions
));
}
internal static class ResourceManagerMemberTemplate
{
private static readonly Lazy<IResourceString> LazyFormat = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString("ResourceManagerMemberTemplate", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value;
public static IResourceString From(IResourceString className, IResourceString resourceManagerTypeName) => AddToCultureCache(new FormattedResourceString(
Format,
GetDefaultCulture,
className,
resourceManagerTypeName
));
}
#region DefaultPropertyName_ResourceManager
private static readonly Lazy<IResourceString> LazyDefaultPropertyName_ResourceManager = new Lazy<IResourceString>(
() => AddToCultureCache(new ResourceManagerString("DefaultPropertyName_ResourceManager", ResourceManager, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString DefaultPropertyName_ResourceManager => LazyDefaultPropertyName_ResourceManager.Value;
#endregion // DefaultPropertyName_ResourceManager
}
}

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="ResourceStringMembers" xml:space="preserve">
<value>
#region {0}
private static readonly Lazy&lt;IResourceString> Lazy{0} = new Lazy&lt;IResourceString>(
() => AddToCultureCache(new ResourceManagerString("{0}", {1}, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString {0} => Lazy{0}.Value;
#endregion // {0}
</value>
<comment>0 = ResourceId, 1 = ResourceManagerPropertyName</comment>
</data>
<data name="ResourceFormatClassMembers" xml:space="preserve">
<value>
internal static class {0}
{{
private static readonly Lazy&lt;IResourceString> LazyFormat = new Lazy&lt;IResourceString>(
() => AddToCultureCache(new ResourceManagerString("{0}", {1}, GetDefaultCulture)),
LazyThreadSafetyMode.PublicationOnly
);
public static IResourceString Format => LazyFormat.Value;
{2}
}}
</value>
<comment>0 = ResourceId, 1 = ResourceManagerPropertyName, 2 = FromMethodDefinition</comment>
</data>
<data name="ResourceFormatClassFromMethod" xml:space="preserve">
<value>
public static IResourceString From({0}) => AddToCultureCache(new FormattedResourceString(
Format,
GetDefaultCulture,
{1}
));
</value>
<comment>0 = FromMethodSignature, 1 = ParameterNames</comment>
</data>
<data name="ResourcesClassTemplate" xml:space="preserve">
<value>
using ResourceString.Net.Contract;
using System;
using System.Globalization;
using System.Resources;
using System.Threading;
namespace {0}
{{
internal static class {1}
{{
{2}
{3}
}}
}}
</value>
<comment>0 = ns, 1 = ClassName, 2 = ResourceManagerRegion, 3 = ResourceRegions</comment>
</data>
<data name="ResourceManagerMemberTemplate" xml:space="preserve">
<value>
#region ResourceManager
private static readonly Type _Type = typeof({1});
private static readonly Lazy&lt;ResourceManager> _ResourceManager = new Lazy&lt;ResourceManager>(
() => new ResourceManager("{0}" ?? string.Empty, _Type.Assembly),
LazyThreadSafetyMode.PublicationOnly
);
public static ResourceManager ResourceManager => _ResourceManager.Value;
private static CultureInfo GetDefaultCulture()
{{
return CultureInfo.CurrentCulture;
}}
private static IResourceString AddToCultureCache(IResourceString source)
{{
return new CultureBasedCachedString(source, GetDefaultCulture);
}}
#endregion // ResourceManager
</value>
<comment>0 = ResourceManagerTypeName, 1 = ClassName</comment>
</data>
<data name="DefaultPropertyName_ResourceManager">
<value>ResourceManager</value>
</data>
</root>

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Deterministic>true</Deterministic>
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LanguageExt.Core" Version="4.4.3" />
<PackageReference Include="System.Resources.Extensions" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../ResourceString.Net.Contract/ResourceString.Net.Contract.csproj" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(MSBuildProjectName).Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>ResourceString.Net.App.Console</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>ResourceString.Net</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,4 @@
global using LanguageExt;
global using ResourceString.Net.Contract;

View File

@@ -0,0 +1,55 @@
using Microsoft.CodeAnalysis;
using ResourceString.Net.Logic.Factories;
using ResourceString.Net.Logic.Parsers.Resx;
namespace ResourceString.Net;
[Generator]
public class Generator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext initContext)
{
// find all additional files that end with .txt
var resxFiles = initContext.AdditionalTextsProvider.Where(static file => file.Path.EndsWith(".resx"));
var assemblies = initContext.CompilationProvider.Select(static (c, _) => c.Assembly);
// read their contents and save their name
var config = resxFiles.Combine(assemblies).Select(
(t, cancellationToken) => (
name: System.IO.Path.GetFileNameWithoutExtension(t.Left.Path),
content: t.Left.GetText(cancellationToken)!.ToString(),
assembly: t.Right,
path: t.Left.Path
)
);
// generate a class that contains their values as const strings
initContext.RegisterSourceOutput(config, (spc, t) =>
{
Parser.TryParse(t.content).IfSome(v =>
{
var relativeNamespace = System.IO.Path.GetDirectoryName(t.path).Substring(
System.IO.Path.GetDirectoryName(t.assembly.Locations[0].GetLineSpan().Path).Length
).Trim(System.IO.Path.DirectorySeparatorChar).Replace(System.IO.Path.DirectorySeparatorChar, '.');
var ns = string.IsNullOrWhiteSpace(relativeNamespace)
? t.assembly.Name
: $"{t.assembly.Name}.{relativeNamespace}";
var resourceFileName = t.name;
var className = CodeSnippetFactory.TransformToClassName(t.name);
var snippet = CodeSnippetFactory.CreateResourceClassCodeSnippet(
CodeSnippetFactory.TransformToNamespace(ns),
className,
CodeSnippetFactory.CreateResourceMangerMemberCodeSnippet(
$"{ns}.{resourceFileName}",
className
),
v.Resources
);
spc.AddSource($"{ns}.{className}", snippet.Value);
});
});
}
}

View File

@@ -0,0 +1,53 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>stubbfel</Authors>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>generator</PackageTags>
<NoPackageAnalysis>true</NoPackageAnalysis>
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageProjectUrl>https://gitlab.com/stubbfel/ResourceString.Net</PackageProjectUrl>
<RepositoryUrl>https://gitlab.com/stubbfel/ResourceString.Net.git</RepositoryUrl>
<RepositoryBranch>main</RepositoryBranch>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>Readme.md</PackageReadmeFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Deterministic>true</Deterministic>
<Optimize>true</Optimize>
</PropertyGroup>
<!-- It puts the dll in the expected folder of the NuGet package to be recognized as a C# analyzer -->
<ItemGroup>
<None Include="$(OutputPath)/*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)/$(AssemblyName).Contract.dll" Pack="true" PackagePath="lib/netstandard2.0" Visible="false" />
<None Include="$(OutputPath)/$(AssemblyName).Contract.pdb" Pack="true" PackagePath="lib/netstandard2.0" Visible="false" />
<None Include="../Readme.md" Pack="true" PackagePath="/"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../ResourceString.Net.Logic\ResourceString.Net.Logic.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,46 @@
{ fetchNuGet }: [
(fetchNuGet { pname = "LanguageExt.Core"; version = "4.4.3"; sha256 = "1pd7wx4c21v56y6i75sxbs990mjrs6bp9h8c48a5w79s1zpbinw5"; })
(fetchNuGet { pname = "Microsoft.Bcl.AsyncInterfaces"; version = "7.0.0"; sha256 = "1waiggh3g1cclc81gmjrqbh128kwfjky3z79ma4bd2ms9pa3gvfm"; })
(fetchNuGet { pname = "Microsoft.CodeAnalysis.Analyzers"; version = "3.3.4"; sha256 = "0wd6v57p53ahz5z9zg4iyzmy3src7rlsncyqpcag02jjj1yx6g58"; })
(fetchNuGet { pname = "Microsoft.CodeAnalysis.Common"; version = "4.3.0"; sha256 = "0qpxygiq53v2d2wl6hccnkjf1lhlxjh4q3w5b6d23aq9pw5qj626"; })
(fetchNuGet { pname = "Microsoft.CodeAnalysis.CSharp"; version = "4.3.0"; sha256 = "0m9qqn391ayfi1ffkzvhpij790hs96q6dbhzfkj2ahvw6qx47b30"; })
(fetchNuGet { pname = "Microsoft.CSharp"; version = "4.7.0"; sha256 = "0gd67zlw554j098kabg887b5a6pq9kzavpa3jjy5w53ccjzjfy8j"; })
(fetchNuGet { pname = "Microsoft.NETCore.Platforms"; version = "1.1.0"; sha256 = "08vh1r12g6ykjygq5d3vq09zylgb84l63k49jc4v8faw9g93iqqm"; })
(fetchNuGet { pname = "Microsoft.NETCore.Targets"; version = "1.1.0"; sha256 = "193xwf33fbm0ni3idxzbr5fdq3i2dlfgihsac9jj7whj0gd902nh"; })
(fetchNuGet { pname = "NETStandard.Library"; version = "2.0.3"; sha256 = "1fn9fxppfcg4jgypp2pmrpr6awl3qz1xmnri0cygpkwvyx27df1y"; })
(fetchNuGet { pname = "System.Buffers"; version = "4.5.1"; sha256 = "04kb1mdrlcixj9zh1xdi5as0k0qi8byr5mi3p3jcxx72qz93s2y3"; })
(fetchNuGet { pname = "System.Collections"; version = "4.3.0"; sha256 = "19r4y64dqyrq6k4706dnyhhw7fs24kpp3awak7whzss39dakpxk9"; })
(fetchNuGet { pname = "System.Collections.Immutable"; version = "6.0.0"; sha256 = "1js98kmjn47ivcvkjqdmyipzknb9xbndssczm8gq224pbaj1p88c"; })
(fetchNuGet { pname = "System.Diagnostics.Contracts"; version = "4.3.0"; sha256 = "1gxawcr4d2y5jmc6y7iv8c1q83hm22f6savcvspvhmpl974jigib"; })
(fetchNuGet { pname = "System.Diagnostics.Debug"; version = "4.3.0"; sha256 = "00yjlf19wjydyr6cfviaph3vsjzg3d5nvnya26i2fvfg53sknh3y"; })
(fetchNuGet { pname = "System.Globalization"; version = "4.3.0"; sha256 = "1cp68vv683n6ic2zqh2s1fn4c2sd87g5hpp6l4d4nj4536jz98ki"; })
(fetchNuGet { pname = "System.IO"; version = "4.3.0"; sha256 = "05l9qdrzhm4s5dixmx68kxwif4l99ll5gqmh7rqgw554fx0agv5f"; })
(fetchNuGet { pname = "System.Linq"; version = "4.3.0"; sha256 = "1w0gmba695rbr80l1k2h4mrwzbzsyfl2z4klmpbsvsg5pm4a56s7"; })
(fetchNuGet { pname = "System.Linq.Expressions"; version = "4.3.0"; sha256 = "0ky2nrcvh70rqq88m9a5yqabsl4fyd17bpr63iy2mbivjs2nyypv"; })
(fetchNuGet { pname = "System.Linq.Queryable"; version = "4.3.0"; sha256 = "0vidv9cjwy8scabxd33mm4zl5vql695rz56ydc42m9b731xi2ahj"; })
(fetchNuGet { pname = "System.Memory"; version = "4.5.4"; sha256 = "14gbbs22mcxwggn0fcfs1b062521azb9fbb7c113x0mq6dzq9h6y"; })
(fetchNuGet { pname = "System.Memory"; version = "4.5.5"; sha256 = "08jsfwimcarfzrhlyvjjid61j02irx6xsklf32rv57x2aaikvx0h"; })
(fetchNuGet { pname = "System.Numerics.Vectors"; version = "4.4.0"; sha256 = "0rdvma399070b0i46c4qq1h2yvjj3k013sqzkilz4bz5cwmx1rba"; })
(fetchNuGet { pname = "System.ObjectModel"; version = "4.3.0"; sha256 = "191p63zy5rpqx7dnrb3h7prvgixmk168fhvvkkvhlazncf8r3nc2"; })
(fetchNuGet { pname = "System.Reflection"; version = "4.3.0"; sha256 = "0xl55k0mw8cd8ra6dxzh974nxif58s3k1rjv1vbd7gjbjr39j11m"; })
(fetchNuGet { pname = "System.Reflection.Emit"; version = "4.7.0"; sha256 = "121l1z2ypwg02yz84dy6gr82phpys0njk7yask3sihgy214w43qp"; })
(fetchNuGet { pname = "System.Reflection.Emit.ILGeneration"; version = "4.3.0"; sha256 = "0w1n67glpv8241vnpz1kl14sy7zlnw414aqwj4hcx5nd86f6994q"; })
(fetchNuGet { pname = "System.Reflection.Emit.ILGeneration"; version = "4.7.0"; sha256 = "0l8jpxhpgjlf1nkz5lvp61r4kfdbhr29qi8aapcxn3izd9wd0j8r"; })
(fetchNuGet { pname = "System.Reflection.Emit.Lightweight"; version = "4.7.0"; sha256 = "0mbjfajmafkca47zr8v36brvknzks5a7pgb49kfq2d188pyv6iap"; })
(fetchNuGet { pname = "System.Reflection.Extensions"; version = "4.3.0"; sha256 = "02bly8bdc98gs22lqsfx9xicblszr2yan7v2mmw3g7hy6miq5hwq"; })
(fetchNuGet { pname = "System.Reflection.Metadata"; version = "5.0.0"; sha256 = "17qsl5nanlqk9iz0l5wijdn6ka632fs1m1fvx18dfgswm258r3ss"; })
(fetchNuGet { pname = "System.Reflection.Primitives"; version = "4.3.0"; sha256 = "04xqa33bld78yv5r93a8n76shvc8wwcdgr1qvvjh959g3rc31276"; })
(fetchNuGet { pname = "System.Reflection.TypeExtensions"; version = "4.3.0"; sha256 = "0y2ssg08d817p0vdag98vn238gyrrynjdj4181hdg780sif3ykp1"; })
(fetchNuGet { pname = "System.Resources.Extensions"; version = "7.0.0"; sha256 = "0d5gk5g5qqkwa728jwx9yabgjvgywsy6k8r5vgqv2dmlvjrqflb4"; })
(fetchNuGet { pname = "System.Resources.ResourceManager"; version = "4.3.0"; sha256 = "0sjqlzsryb0mg4y4xzf35xi523s4is4hz9q4qgdvlvgivl7qxn49"; })
(fetchNuGet { pname = "System.Runtime"; version = "4.3.0"; sha256 = "066ixvgbf2c929kgknshcxqj6539ax7b9m570cp8n179cpfkapz7"; })
(fetchNuGet { pname = "System.Runtime.CompilerServices.Unsafe"; version = "4.5.3"; sha256 = "1afi6s2r1mh1kygbjmfba6l4f87pi5sg13p4a48idqafli94qxln"; })
(fetchNuGet { pname = "System.Runtime.CompilerServices.Unsafe"; version = "6.0.0"; sha256 = "0qm741kh4rh57wky16sq4m0v05fxmkjjr87krycf5vp9f0zbahbc"; })
(fetchNuGet { pname = "System.Runtime.Extensions"; version = "4.3.0"; sha256 = "1ykp3dnhwvm48nap8q23893hagf665k0kn3cbgsqpwzbijdcgc60"; })
(fetchNuGet { pname = "System.Text.Encoding"; version = "4.3.0"; sha256 = "1f04lkir4iladpp51sdgmis9dj4y8v08cka0mbmsy0frc9a4gjqr"; })
(fetchNuGet { pname = "System.Text.Encoding.CodePages"; version = "6.0.0"; sha256 = "0gm2kiz2ndm9xyzxgi0jhazgwslcs427waxgfa30m7yqll1kcrww"; })
(fetchNuGet { pname = "System.Threading"; version = "4.3.0"; sha256 = "0rw9wfamvhayp5zh3j7p1yfmx9b5khbf4q50d8k5rk993rskfd34"; })
(fetchNuGet { pname = "System.Threading.Tasks"; version = "4.3.0"; sha256 = "134z3v9abw3a6jsw17xl3f6hqjpak5l682k2vz39spj4kmydg6k7"; })
(fetchNuGet { pname = "System.Threading.Tasks.Extensions"; version = "4.5.4"; sha256 = "0y6ncasgfcgnjrhynaf0lwpkpkmv4a07sswwkwbwb5h7riisj153"; })
(fetchNuGet { pname = "System.ValueTuple"; version = "4.5.0"; sha256 = "00k8ja51d0f9wrq4vv5z2jhq8hy31kac2rg0rv06prylcybzl8cy"; })
]

7
alias.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/env sh
alias nixe='nix --experimental-features "nix-command flakes"'
alias nulock='nixe run .#devTasks.updateNugetLock'
alias fllock='nixe run .#devTasks.updateFlakeLock'
alias ulock='nulock && fllock'
alias restore-on-current-source='nixe build && dotnet nuget locals all -c && dotnet restore -s result/share -s https://api.nuget.org/v3/index.json $1'

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<PropertyGroup>
<AdditionalFileItemNames>$(AdditionalFileItemNames);EmbeddedResource</AdditionalFileItemNames>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Resources.Extensions" Version="7.0.0" />
<PackageReference Include="ResourceString.Net" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="local-packages" value="../../ResourceString.Net/bin/Debug" />
</packageSources>
</configuration>

View File

@@ -0,0 +1,22 @@
namespace Test
{
internal class Program
{
private static void Main(string[] args)
{
Console.WriteLine("Hello");
var foo = Example.App.Resources.Test1;
Console.WriteLine(foo.Value);
var bar = Example.App.Properties.SubProperties.Properties.Test2.From(
ResourceString.Net.Contract.LiteralString.Factory("1"),
ResourceString.Net.Contract.LiteralString.Factory("2")
);
Console.WriteLine(bar.Value);
}
}
}

View File

@@ -0,0 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<root>
<data name='Test1'>
<value>Value1</value>
<comment>This is a greeting message.</comment>
</data>
<data name='Test2' type='System.String'>
<value>{1}Value2{0}{{2}}</value>
<comment>2 = prefix</comment>
</data>
<data name='Test3' type='System.Byte[]'>
<value>0xDEADBEEF</value>
</data>
</root>

View File

@@ -0,0 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<root>
<data name='Test1'>
<value>Value1</value>
<comment>This is a greeting message.</comment>
</data>
<data name='Test2' type='System.String'>
<value>{1}Value2{0}{{2}}</value>
<comment>2 = prefix</comment>
</data>
<data name='Test3' type='System.Byte[]'>
<value>0xDEADBEEF</value>
</data>
</root>

26
flake.lock generated Normal file
View File

@@ -0,0 +1,26 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1684522302,
"narHash": "sha256-L7nUSrOYTWvXmIQ8NtVU2/AAah/ouJpf9DDVSt0s9+I=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c555a28f2436be370c40df70f4cd6c25fceff7af",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.11-small",
"type": "indirect"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

157
flake.nix Normal file
View File

@@ -0,0 +1,157 @@
{
description = "";
inputs.nixpkgs.url = "nixpkgs/nixos-22.11-small";
outputs = { self, nixpkgs }:
let
name = "ResourceString.Net";
version = "0.0.4";
supportedSystems =
[ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
scripts = forAllSystems (system: {
updateFlakeLockFile = nixpkgsFor.${system}.writeScript "update_flake_lock.sh" ''
nix --experimental-features 'nix-command flakes' flake lock --update-input nixpkgs
nix --experimental-features 'nix-command flakes' build
'';
updateNugetLockFile = nixpkgsFor.${system}.writeScript "update_nuget_lock.sh" ''
temp_package_folder=$(mktemp -d)
dotnet restore ResourceString.Net --packages "$temp_package_folder"
${nixpkgsFor.${system}.nuget-to-nix}/bin/nuget-to-nix "$temp_package_folder" > ResourceString.Net/deps.nix
rm -rf "$temp_package_folder"
'';
autoTag = nixpkgsFor.${system}.writeScript "auto_tag.sh" ''
git tag --force v${version}
git push origin v${version}
'';
updateLogicResourceFile = nixpkgsFor.${system}.writeScript "update_logic_resource_file.sh" ''
dotnet run --project ResourceString.Net.App.Console ResourceString.Net.Logic/Properties/Resources.resx ResourceString.Net.Logic.Properties > ResourceString.Net.Logic/Properties/Resources.cs.tmp
mv ResourceString.Net.Logic/Properties/Resources.cs.tmp ResourceString.Net.Logic/Properties/Resources.cs
'';
runAllTests = nixpkgsFor.${system}.writeScript "run_all_tests.sh" ''
dotnet test *.Tests
'';
buildAllAssemblies = nixpkgsFor.${system}.writeScript "build_all_assemblies.sh" ''
# Find all .csproj files below the current folder
csproj_files=$(find . -name '*.csproj')
# Create a temporary file to store the number of dependencies for each project
temp_file=$(mktemp)
# Loop through each .csproj file and count its number of ResourceString dependencies
for file in $csproj_files
do
count=$(cat "$file" | grep -c "<ProjectReference.*ResourceString")
depth=$(echo "$file" | tr -cd '/' | wc -c)
dot_count=$(echo "$file" | tr -cd '.' | wc -c)
echo "$depth $count $dot_count $file" >> "$temp_file"
done
# Sort the projects by their count of dependencies and path depth, in ascending order
sorted_projects=$(sort -nk1 -nk2 -nk3 -t' ' "$temp_file" | awk '{print $4}')
# Loop through the sorted projects and build them
while read -r file
do
echo "Building project: $file"
dotnet build "$file"
done << EOF
$sorted_projects
EOF
# Remove the temporary file
rm "$temp_file"
'';
cleanAllAssemblies = nixpkgsFor.${system}.writeScript "clean_all_assemblies.sh" ''
csproj_files=$(find . -name '*.csproj')
for file in $csproj_files
do
dotnet clean "$file"
done
'';
runDefaultApp = nixpkgsFor.${system}.writeScript "run_default_app.sh" ''
dotnet run --project ResourceString.Net.App.Console $@
'';
createNugetPackage = nixpkgsFor.${system}.writeScript "run_default_app.sh" ''
dotnet run --project ResourceString.Net.App.Console $@
'';
});
in
rec {
packages = forAllSystems (system: {
default = nixpkgsFor.${system}.buildDotnetModule {
pname = name;
version = version;
src = self;
projectFile = [
"ResourceString.Net/ResourceString.Net.csproj"
"ResourceString.Net.App.Console/ResourceString.Net.App.Console.csproj"
];
nugetDeps = ./ResourceString.Net/deps.nix;
projectReferences = [ ];
dotnet-sdk = nixpkgsFor.${system}.dotnet-sdk;
dotnet-runtime = nixpkgsFor.${system}.dotnet-runtime;
executables = [ "ResourceString.Net.App.Console" ];
packNupkg = true;
runtimeDeps = [ ];
};
});
apps = forAllSystems (system: {
default = {
type = "app";
program = "${scripts.${system}.runDefaultApp}";
};
devTasks = {
updateFlakeLock = {
type = "app";
program = "${scripts.${system}.updateFlakeLockFile}";
};
updateNugetLock = {
type = "app";
program = "${scripts.${system}.updateNugetLockFile}";
};
autoTag = {
type = "app";
program = "${scripts.${system}.autoTag}";
};
updateLogicResourceFile = {
type = "app";
program = "${scripts.${system}.updateLogicResourceFile}";
};
runAllTests = {
type = "app";
program = "${scripts.${system}.runAllTests}";
};
buildAllAssemblies = {
type = "app";
program = "${scripts.${system}.buildAllAssemblies}";
};
cleanAllAssemblies = {
type = "app";
program = "${scripts.${system}.cleanAllAssemblies}";
};
};
});
devShells = forAllSystems (system: {
default = nixpkgsFor.${system}.mkShell {
name = "dev-shell";
packages =
[ nixpkgsFor.${system}.dotnet-sdk nixpkgsFor.${system}.nixfmt ];
shellHook = ''
alias nixe="nix --experimental-features 'nix-command flakes'"
'';
};
});
};
}