The current localization is a bit outdated. Recreation of Resources.Designer.cs causes git to think it changed. Removing this file alone does not allow build without explicit recreation.
There is an approach in:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization/make-content-localizable?view=aspnetcore-9.0
Migrate from .resx + *.Designer.cs to IStringLocalizer (no designer files)
Type: refactor ♻️ · Area: localization 🌍 · Priority: high
Goal: Eliminate *.Designer.cs churn/diffs and adopt the ASP.NET Core-native localization stack (IStringLocalizer / IViewLocalizer), keeping only .resx files in source control.
Why
*.Designer.cs are generated and frequently re-written across branches/VS versions → noisy diffs and merge conflicts.
- ASP.NET Core supports localization without designer classes using
IStringLocalizer (runtime lookup from embedded .resx).
- Cleaner repo: only
.resx committed; generated code stays out of Git.
Out of scope
- Changing actual translation keys/wording (we keep existing keys).
- Re-structuring routes or business logic.
Plan (high level)
- Remove legacy designer files (delete from repo & project).
- Wire up localization in
Program.cs.
- Move/confirm resource files under
Resources/ and ensure Build Action = Embedded resource.
- Replace calls to
Resources.MyStrings.SomeKey (designer props) with _localizer["SomeKey"] (or Localizer["SomeKey"] in views).
- Update DataAnnotations and validation messages to use localization.
- Add MSTest tests (with NSubstitute) for culture switching.
- Update CI: ensure resx are embedded and culture tests run.
- Add
.gitignore rules after deleting designer files.
Important ordering: First delete all *.Designer.cs and references, commit that, then add .gitignore entries so they aren’t reintroduced.
Detailed tasks & checklists
0) Remove designer files (first!)
<!-- In .csproj -->
<ItemGroup>
<Compile Remove="**\*.Designer.cs" />
</ItemGroup>
1) Enable localization services & middleware
// Program.cs (.NET 8)
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Options;
using System.Globalization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddControllersWithViews()
.AddViewLocalization()
.AddDataAnnotationsLocalization();
var app = builder.Build();
var supportedCultures = new[] { "en", "de" }; // extend as needed
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture("en")
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Acceptance: App chooses translations based on Accept-Language header or explicit culture.
2) Resource file layout
// /Resources/SharedResource.cs
namespace MyApp.Resources;
public sealed class SharedResource { }
3) Replace designer usages
Before (designer style):
ViewBag.Title = Resources.MyStrings.PageTitle; // property from Designer.cs
var msg = Resources.ValidationTexts.RequiredFieldError; // property from Designer.cs
After (localizer style):
using Microsoft.Extensions.Localization;
using MyApp.Resources;
public class HomeController : Controller
{
private readonly IStringLocalizer<SharedResource> _L;
public HomeController(IStringLocalizer<SharedResource> localizer) => _L = localizer;
public IActionResult Index()
{
ViewBag.Title = _L["PageTitle"];
var msg = _L["RequiredFieldError"];
return View();
}
}
In Razor views:
@using MyApp.Resources
@inject Microsoft.Extensions.Localization.IViewLocalizer Localizer
<h1>@Localizer["PageTitle"]</h1>
<p>@Localizer["WelcomeText"]</p>
In non-MVC services:
using Microsoft.Extensions.Localization;
using MyApp.Resources;
public class MailSender
{
private readonly IStringLocalizer<SharedResource> _L;
public MailSender(IStringLocalizer<SharedResource> L) => _L = L;
public Task SendWelcomeAsync(string email)
{
var subject = _L["WelcomeSubject"];
var body = _L["WelcomeBody"];
// ...
return Task.CompletedTask;
}
}
4) DataAnnotations & validation messages
Before:
[Required(ErrorMessageResourceType = typeof(Resources.ValidationTexts),
ErrorMessageResourceName = "Required")]
public string Name { get; set; }
After (preferred):
// Use DataAnnotationsLocalization; provide resource overrides if needed
[Required]
public string Name { get; set; }
5) Tests (MSTest + NSubstitute)
<!-- In test .csproj -->
<ItemGroup>
<PackageReference Include="MSTest.TestAdapter" Version="3.6.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.6.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="$(MicrosoftExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsVersion)" />
</ItemGroup>
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyApp.Resources;
namespace MyApp.Tests.Localization
{
[TestClass]
public class LocalizationSmokeTests
{
[TestMethod]
public void SharedResource_ReturnsGerman_WhenUICultureIsDe()
{
var services = new ServiceCollection()
.AddLocalization(o => o.ResourcesPath = "Resources")
.BuildServiceProvider();
var L = services.GetRequiredService<IStringLocalizer<SharedResource>>();
var saved = CultureInfo.CurrentUICulture;
try
{
CultureInfo.CurrentUICulture = new CultureInfo("de");
Assert.AreEqual("Hallo", L["Hello"]); // adjust to your expected value
}
finally
{
CultureInfo.CurrentUICulture = saved;
}
}
}
}
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
using Microsoft.Extensions.Localization;
using MyApp.Resources;
namespace MyApp.Tests.Localization
{
public interface IGreeter
{
string Greet();
}
[TestClass]
public class LocalizerConsumerTests
{
[TestMethod]
public void Greeter_UsesLocalizerKey()
{
var L = Substitute.For<IStringLocalizer<SharedResource>>();
L["Welcome"].Returns(new LocalizedString("Welcome", "Welcome (mock)"));
var greeter = new Greeter(L);
Assert.AreEqual("Welcome (mock)", greeter.Greet());
}
private sealed class Greeter : IGreeter
{
private readonly IStringLocalizer<SharedResource> _L;
public Greeter(IStringLocalizer<SharedResource> L) => _L = L;
public string Greet() => _L["Welcome"];
}
}
}
6) CI/CD updates
# scripts/verify-no-designer.sh
set -euo pipefail
if git ls-files '*.Designer.cs' | grep -q .; then
echo "Error: *.Designer.cs files found in repo. Remove them."
exit 1
fi
Invoke in CI before build/tests.
7) Add .gitignore rules (after deletion)
Do this after all designer files have been deleted and committed.
# generated designer files (legacy)
*.Designer.cs
# .gitattributes
* text=auto eol=lf
Notes on resource scoping
IStringLocalizer<SharedResource> → single shared resource pool for the app.
IStringLocalizer<HomeController> → per-type resources (Resources/Controllers.HomeController.*.resx).
- Mix both patterns as needed.
Optional: strongly-typed keys without Designer files
Consider a ResX Source Generator for compile-time safety without committing generated code.
Rollback plan
- Re-introduce designer files by setting
Custom Tool on .resx back to ResXFileCodeGenerator in Visual Studio, re-add *.Designer.cs to the project, and remove ignore rule.
- Restore previous code paths referencing
Resources.*.
Acceptance criteria
Effort
- S (if few usages) to M (if many references). Grep-replace + targeted fixes usually suffice.
The current localization is a bit outdated. Recreation of
Resources.Designer.cscauses git to think it changed. Removing this file alone does not allow build without explicit recreation.There is an approach in:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization/make-content-localizable?view=aspnetcore-9.0
Migrate from
.resx+*.Designer.cstoIStringLocalizer(no designer files)Type: refactor ♻️ · Area: localization 🌍 · Priority: high
Goal: Eliminate
*.Designer.cschurn/diffs and adopt the ASP.NET Core-native localization stack (IStringLocalizer/IViewLocalizer), keeping only.resxfiles in source control.Why
*.Designer.csare generated and frequently re-written across branches/VS versions → noisy diffs and merge conflicts.IStringLocalizer(runtime lookup from embedded.resx)..resxcommitted; generated code stays out of Git.Out of scope
Plan (high level)
Program.cs.Resources/and ensure Build Action = Embedded resource.Resources.MyStrings.SomeKey(designer props) with_localizer["SomeKey"](orLocalizer["SomeKey"]in views)..gitignorerules after deleting designer files.Detailed tasks & checklists
0) Remove designer files (first!)
*.Designer.csgenerated from.resx..csprojhas no<Compile Include="...Designer.cs" />..gitignoreyet; do that after this commit).1) Enable localization services & middleware
Program.cs:Acceptance: App chooses translations based on
Accept-Languageheader or explicit culture.2) Resource file layout
Resources/at the project root if it doesn’t exist.Resources/SharedResource.resx(neutral)Resources/SharedResource.de.resx,Resources/SharedResource.en.resx, …Resources/Controllers.HomeController.resx,Resources/Controllers.HomeController.de.resx, ….resxhas Build Action = Embedded resource (default in SDK-style projects).3) Replace designer usages
Before (designer style):
After (localizer style):
In Razor views:
In non-MVC services:
Resources.*.with_L["..."]/Localizer["..."]..resx“Name” column.4) DataAnnotations & validation messages
Before:
After (preferred):
AddDataAnnotationsLocalization()is configured.RequiredAttribute_ValidationErrorinResources/to override defaults, or injectIStringLocalizerin custom validators.5) Tests (MSTest + NSubstitute)
6) CI/CD updates
EmbeddedResourcebehavior for.resx.*.Designer.csis present:Invoke in CI before build/tests.
7) Add
.gitignorerules (after deletion).gitignore:Notes on resource scoping
IStringLocalizer<SharedResource>→ single shared resource pool for the app.IStringLocalizer<HomeController>→ per-type resources (Resources/Controllers.HomeController.*.resx).Optional: strongly-typed keys without Designer files
Consider a ResX Source Generator for compile-time safety without committing generated code.
Rollback plan
Custom Toolon.resxback toResXFileCodeGeneratorin Visual Studio, re-add*.Designer.csto the project, and remove ignore rule.Resources.*.Acceptance criteria
*.Designer.csin the repo.IStringLocalizerorIViewLocalizer.Accept-Language: devsenreturns localized content.*.Designer.csexists.Effort