Skip to content

Add LocalizedString.Create and LocalizedHtmlString.Create - #19933

Closed
MikeAlhayek wants to merge 3 commits into
mainfrom
ma/localized-string-create
Closed

MikeAlhayek wants to merge 3 commits into
mainfrom
ma/localized-string-create

Conversation

@MikeAlhayek

@MikeAlhayek MikeAlhayek commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Summary

Some APIs need a LocalizedString or a LocalizedHtmlString, but there is no localizer where the value is created. Today the code repeats the same text for the name and the value:

entry.DisplayName = new LocalizedString(providerName, providerName);

This PR adds a static Create(string value, params object[] arguments) method to both types, as C# 14 extension members, so that the text is written only once:

entry.DisplayName = LocalizedString.Create(providerName);
var message = LocalizedHtmlString.Create("<strong>Hello</strong>");
var greeting = LocalizedString.Create("Hello {0}", userName);

The value is also used as the name. This works because Orchard Core uses the English text both as the default value and as the name of a localized string.

This is the first PR of a stack:

  1. This PR: add LocalizedString.Create and LocalizedHtmlString.Create.
  2. Use LocalizedString.Create for deployment step categories #19934: use LocalizedString.Create for the category of every deployment step.
  3. Use LocalizedString for permission descriptions #19936: use LocalizedString for permission descriptions.

Changes

  • LocalizedStringExtensions in the Microsoft.Extensions.Localization namespace, and LocalizedHtmlStringExtensions in the Microsoft.AspNetCore.Mvc.Localization namespace. Both are in OrchardCore.Abstractions/Localization/Extensions. Every Orchard Core project references OrchardCore.Abstractions, so the extensions can be used everywhere, including in OrchardCore.Infrastructure.Abstractions. They use the same namespaces as the types. Code that already uses the types can call LocalizedString.Create(...) with no new using directive.
  • Create(value) is the same as new LocalizedString(value, value) and new LocalizedHtmlString(value, value). It does not translate the value, and ResourceNotFound is false.
  • Like S["...", a, b] and H["...", a, b], Create accepts optional format arguments. The name stays the unformatted text. For a LocalizedString, the value is formatted with the arguments. For a LocalizedHtmlString, the arguments are HTML encoded when the string is rendered. With no arguments, the text is not formatted, so "Hello {0}" does not throw.
  • Code that does not use C# 14 can call LocalizedStringExtensions.Create(...) or LocalizedHtmlStringExtensions.Create(...).
  • Docs: a new "Creating a localized string without a localizer" section in reference/modules/Localize/README.md, and a 4.0.0 release notes entry.

Tests

  • New LocalizedStringExtensionsTests and LocalizedHtmlStringExtensionsTests in OrchardCore.Tests.
  • dotnet test --project test/OrchardCore.Tests -c Release --filter-class "*LocalizedStringExtensionsTests" --filter-class "*LocalizedHtmlStringExtensionsTests" --filter-class "*NullStringLocalizerTests": 33 passed.

Related

The PO extractor will pick up LocalizedString.Create("...") and LocalizedHtmlString.Create("...") when the argument is literal text: OrchardCoreContrib/OrchardCoreContrib.PoExtractor#130

🤖 Generated with Claude Code

@hishamco

Copy link
Copy Markdown
Member

Please hold on .. we might need to discuss this with @sebastienros. If there's an option within the UI to change this, then DataLocalizer might fit here; otherwise will check the HTML localizer

Comment thread src/docs/releases/4.0.0.md Resolved

@sebastienros sebastienros left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pending comment reviews

@MikeAlhayek
MikeAlhayek force-pushed the ma/localized-string-create branch from a02f73d to d8eb2e2 Compare September 24, 2026 21:04
{
ArgumentNullException.ThrowIfNull(value);

return new(value, value, isResourceNotFound: false, arguments ?? []);

@hishamco hishamco Sep 24, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is weird, because the resource is not found, so the question is where the localization string should come from?

@sebastienros, how does this align with localization strings that come from PO files?!!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Skrypt, any thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is not found it will probably take the T["english string"] directly.

MikeAlhayek and others added 3 commits September 29, 2026 16:13
Add C# 14 static extension members that create a LocalizedString or a
LocalizedHtmlString where the name is also the value. The extensions
live in the same namespaces as the types so they are discoverable
without an extra using directive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OrchardCore.Abstractions is referenced by every Orchard Core project,
so the extensions can be used everywhere, including in
OrchardCore.Infrastructure.Abstractions where Permission is declared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rename the parameter of LocalizedString.Create and
  LocalizedHtmlString.Create from name to value: the text is the value,
  and it is also used as the name.
- Accept optional format arguments, like S["...", a, b] and
  H["...", a, b]. The name stays the unformatted text.
- Explain in the docs why the value can be used as the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/// <remarks>
/// The returned string is not translated. The name can be used as the key to localize the string later.
/// </remarks>
public static LocalizedString Create(string value, params object[] arguments)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't be right, the values of the arguments can only be provided on render, and the same localization string can be rendered many times with different values.

sebastienros added a commit that referenced this pull request Oct 3, 2026
* Use immutable sources for deferred localization

Supersedes #19933, #19934, #19936.

Introduce OrchardCore.Localization.LocalizationSource, a sealed record
pairing an untranslated value with the Type that supplies its PO
context. Add IStringLocalizerFactory.Localize and
IHtmlLocalizerFactory.Localize extensions that call Create(source.Type)
and defer text/HTML encoding and formatting to render time.

Permission.Description and DeploymentStep.Category/Title now use
LocalizationSource. All 53 built-in deployment steps cache their 85
category/title sources in private static readonly fields assigned in
parameterless constructors, avoiding localizer injection purely for
labels. The default empty deployment category is shared as well.
Context-free sources preserve legacy plain-string Permission
constructors and Data Localization of dynamically generated
permissions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Add factory-based plural support for LocalizationSource

Add IStringLocalizerFactory.Plural and IHtmlLocalizerFactory.Plural
extensions in OrchardCore.Localization.Abstractions, taking a count, a
singular LocalizationSource, the plural text, and optional format
arguments. For context-bearing sources they resolve
factory.Create(source.Type) and delegate to the existing
IStringLocalizer.Plural/IHtmlLocalizer.Plural extensions, so
culture-specific PO plural rules (including zero forms and more than
two forms) select the translated form. For context-free sources they
fall back to the English singular/plural rule, matching
NullStringLocalizer semantics, without a reverse dependency on the
higher-level OrchardCore assembly. Sources remain immutable; count is
always the first format argument.

Add targeted tests covering null validation, context-free fallback,
context-bearing delegation, source immutability, string-vs-HTML
encoding, and culture-specific zero/few/many forms using a real
PortableObjectStringLocalizerFactory with a new Russian pluralization
rule test helper. Update the Localize docs and revise the 4.0.0
release notes entry in place.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Add explicit LocalizationSource creation factories

Use Create and Create<T> declarations for discoverable localization sources while preserving original keys, contexts, and shared instances.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Fix localization source factory analyzer violations

Prefer generic factories for nonstatic contexts and retain runtime-Type and static-context coverage. Use the concrete localized HTML type in the plural test renderer.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Sébastien Ros <1165805+sebastienros@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants