Localizes OrchardCore apps — UI strings (IStringLocalizer/IViewLocalizer S, T, H), LocalizationSource, updating and exporting checked-in POT templates, localization analyzers, PO files and pluralization, content translation (LocalizationPart, culture picker), and supported-culture configuration. Use when adding or changing localizable strings, updating POT files, fixing OCLOC diagnostics, translating strings, adding a culture, or making content multilingual.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
This skill guides you through OrchardCore localization following project conventions.
Three layers, often confused:
IStringLocalizer (S), IViewLocalizer (T), IHtmlLocalizer (H). Checked-in POT templates describe the source strings and are embedded in assemblies; translated PO files supply runtime translations.LocalizationPart and the OrchardCore.ContentLocalization module.| Goal | Layer |
|---|---|
| Translate a label/message in code or a view | UI localization (S/T/H) + PO file |
| Pluralize ("1 item" / "{0} items") | S.Plural(...) + PO msgid_plural |
| Add, change, remove, or move a source string | Update the owning project's checked-in POT template (Workflow D) |
| Export source strings for Crowdin or another translation platform | Export POT templates (Workflow D), then deploy translated PO files |
| Translate a blog post into French | Content localization (LocalizationPart) |
| Add a supported language / set default | Localization settings (LocalizationSettings) |
| Translate data-driven values (e.g. taxonomy terms) | Data Localization (DataLocalization module) |
| Localizer | Convention | Where |
|---|---|---|
IStringLocalizer<T> | field S | drivers, services, controllers |
IViewLocalizer | property T | Razor views (already on RazorPage) |
IHtmlLocalizer<T> | field H | when the translation contains HTML |
public sealed class AliasPartDisplayDriver : ContentPartDisplayDriver<AliasPart>
{
internal readonly IStringLocalizer S;
public AliasPartDisplayDriver(IStringLocalizer<AliasPartDisplayDriver> localizer)
=> S = localizer;
}In Razor: @T["Welcome to Orchard Core"]. In a class: S["A value is required for {0}.", name]. Keep keys as compile-time constants and pass values as format arguments, not interpolated keys. In Liquid: {{ "Welcome to Orchard Core" | t }}.
S.Plural(count, "{0} book", "{0} books");
// IHtmlLocalizer: H.Plural(count, "{0} book", "{0} books")Strings declared before a localizer is available, such as deployment step categories and titles, are LocalizationSource instances translated at display time with IStringLocalizerFactory.Localize(). Don't declare them in the class that uses them:
CommonLocalizationSources (OrchardCore.Abstractions, namespace OrchardCore.Localization) for text that several projects use with the same meaning, such as CommonLocalizationSources.ContentManagement, or that is general to every module, such as CommonLocalizationSources.Settings. Look for an existing member before adding one. Add a member only when another project uses the text too; a module name or a category used by one project goes in that project's class.internal static class {Area}LocalizationSources at the root of the module, for example LuceneLocalizationSources, with LocalizationSource.Create("...", typeof({Area}LocalizationSources)), which extraction tools can find.*Permissions class, and strings specific to a view in the view, with T.[JsonIgnore] // Not inherited from the base property; LocalizationSource can't be serialized.
public override LocalizationSource Category => CommonLocalizationSources.Search;
[JsonIgnore]
public override LocalizationSource Title => LuceneLocalizationSources.LuceneSearchIndexes;The PO context is the class that declares the source, for example msgctxt "OrchardCore.Localization.CommonLocalizationSources".
Resolve a source at display time with stringLocalizerFactory.Localize(source, args) or htmlLocalizerFactory.Localize(source, args), using the current tenant's factory. Pass arguments to the factory extension, not to LocalizationSource.Create; never cache a translated result in a static field. For plurals, use stringLocalizerFactory.Plural(count, source, "{0} items") (or the HTML factory equivalent), with a constant plural form. The singular source must have a context; count supplies {0} automatically.
PO files are discovered (in order) from each module's Localization/, a global /Localization, the tenant's App_Data/Sites/{tenant}/Localization, and culture folders. A PO entry:
msgctxt "OrchardCore.Admin.Views.AdminDashboard"
msgid "Welcome to Orchard Core"
msgstr "Bienvenue sur Orchard Core"msgctxt = {Namespace}.{Class} (services) or {Namespace}.{ViewPath} (views). Plurals:
msgctxt "OrchardCore.AdminMenu.Views.Menu.List"
msgid "1 item"
msgid_plural "{0} items"
msgstr[0] "{0} élément"
msgstr[1] "{0} éléments"See references/localization.md for the full PO discovery order and headers.
After adding, changing, removing, or moving a localized string, update the affected project's POT file using Workflow D below. Context changes and moving shared sources can change translation identities even when the displayed text stays the same.
Enable OrchardCore.ContentLocalization (Content Localization). Attach LocalizationPart to the content type (admin or recipe). It carries:
public class LocalizationPart : ContentPart, ILocalizable
{
public string LocalizationSet { get; set; } // links all translations
public string Culture { get; set; } // this version's culture
}On create, the part handler generates a unique LocalizationSet and sets the default culture automatically.
From the editor's Localization, "Translate" creates a sibling content item sharing the same LocalizationSet with a different Culture. Programmatically:
var translated = await _contentLocalizationManager.LocalizeAsync(contentItem, "fr-FR");IContentLocalizationManager fetches across a set:
await _contentLocalizationManager.GetContentItemAsync(localizationSet, "fr");
await _contentLocalizationManager.GetItemsForSetAsync(localizationSet);Liquid: {{ Model.ContentItem.Content.LocalizationPart.LocalizationSet | localization_set: "en" }}. The Content Culture Picker feature adds a front-end language switcher and a ContentRequestCultureProvider that derives the culture from the localized route.
Recipe settings step:
{
"name": "settings",
"LocalizationSettings": {
"DefaultCulture": "en-US",
"SupportedCultures": [ "en-US", "fr-FR", "es-ES" ],
"FallBackToParentCulture": true
}
}The Localization module reads these at startup and calls app.UseRequestLocalization(...) with the configured default + supported cultures.
In this repository, OrchardCore.Commons.props already imports the localization targets for framework projects, modules, and themes. Do not add redundant imports or package references. Outside the repository, each project whose strings need extraction must reference OrchardCore.Localization.Build with PrivateAssets="all" and a version matching Orchard Core.
Each project's template is <AssemblyName>.pot, next to its project file. A normal build only embeds an existing template as <AssemblyName>.Localization.pot; it neither extracts strings nor refreshes a stale template. A project without a template embeds nothing. GenerateLocalizationCatalog=true enables this integration, not automatic extraction.
Run commands from the repository root. Prefer updating just the affected project:
dotnet msbuild src\OrchardCore.Modules\OrchardCore.Admin\OrchardCore.Admin.csproj -t:UpdateLocalizationCatalogUse -restore with dotnet msbuild when dependencies need restoring. The target updates only that project's template, not those of its references. For an application and its project references, or the entire solution:
dotnet build src\OrchardCore.Cms.Web\OrchardCore.Cms.Web.csproj -p:UpdateLocalizationCatalogs=true
dotnet build OrchardCore.slnx -p:UpdateLocalizationCatalogs=trueChoose the appropriate scope, rather than running every command. Use the SDK selected by global.json: extraction loads that SDK's Roslyn and Razor compilers. The source-tree worker is built on demand; no separate PoExtractor installation is needed.
Updates create or rewrite the template, or remove it if no messages remain. Unchanged inputs reuse the cache under obj\; unchanged output is not rewritten. Do not hand-edit generated entries, headers, or references. Templates use deterministic ordering, UTF-8 and LF, no timestamps, and file references without line numbers by default. Multi-targeted projects extract using their first target framework; DEBUG and TRACE are excluded during extraction.
git status --short -- '*.pot'
git --no-pager diff -- '*.pot'Review new files too (a normal git diff does not show untracked templates). Check the expected msgctxt, msgid, msgid_plural, source references, format flags, and format-argument comments. Keep msgstr values empty in POT files; translations belong in culture-specific PO files. Commit added, modified, and deleted POT files with their source changes. CI updates the solution's templates and fails if any are uncommitted or out of date.
Extraction covers C#, Razor, literal Liquid t inputs, context-bearing LocalizationSource declarations, and explicit constant DataAnnotations validation messages. It does not extract JavaScript/TypeScript directly: expose their strings through a C# IJSLocalizer implementation. Fix dynamic keys or unresolved contexts at the source instead of inserting guessed POT entries.
IDE/build analyzers report OCLOC101 (constant source missing a context), OCLOC102 (different declaring context, informational), OCLOC103 (nonconstant source value), and OCLOC104 (nonconstant localizer key or plural forms). The update worker reports OCLOC001-OCLOC009; see the reference for their meanings. Extraction errors always fail; -p:LocalizationStrict=true also fails on extraction warnings.
Use [SkipLocalizationExtraction] only on forwarding infrastructure whose keys are declared elsewhere; it excludes marked classes/methods and nested declarations from extraction and analyzer diagnostics, not application calls to those helpers. For an intentionally dynamic application call, locally suppress OCLOC104 with an explanation of where its keys originate. Do not hide missing application strings with broad suppressions.
Build with an absolute, dedicated output directory to collect existing templates without regenerating them:
$output = Join-Path $PWD 'artifacts\localization'
dotnet build OrchardCore.slnx "-p:LocalizationCatalogOutputPath=$output"Files are copied to <output>\<TargetFramework>\<AssemblyName>.pot. Add -p:UpdateLocalizationCatalogs=true only when an update is also intended. Use an empty export directory for an exact current snapshot; export does not remove stale files. Only built projects contribute templates, not prebuilt NuGet dependencies. Upload POT files to the translation platform, preserve contexts/plurals/placeholders, and download translated PO files to a supported runtime localization location. Embedded POT files alone do not change runtime translations.
For configuration properties, custom Liquid inputs, extraction diagnostics, and library entry points, see references/localization.md.
| Symbol | Type | Output |
|---|---|---|
S | IStringLocalizer<T> | plain string |
T | IViewLocalizer | encoded view string |
H | IHtmlLocalizer<T> | trusted translation markup; format arguments are HTML-encoded |
| Member | Purpose |
|---|---|
IContentLocalizationManager.LocalizeAsync(item, culture) | create a translation |
.GetContentItemAsync(set, culture) | one translation |
.GetItemsForSetAsync(set) | all translations |
.GetItemsForSetsAsync(sets, culture) | batch |
ILocalizationService.GetDefaultCultureAsync() / GetSupportedCulturesAsync() | culture config |
| Provider | Picks culture from |
|---|---|
ContentRequestCultureProvider | the localized content route (added first) |
AdminCookieCultureProvider | admin_culture_* cookie, admin paths only |
| cookie / querystring / Accept-Language | ASP.NET Core defaults |
localization_set: "fr" — translated item; switch_culture_url — culture-switch URL.
msgctxt must exactly match the namespace+class (or namespace+view path). Wrong context → string stays untranslated.IStringLocalizer<T> context comes from T's type — inject the localizer typed to the class that owns the strings.LocalizationSet links translations; never share one set across unrelated items, and don't overwrite it.SupportedCultures before content can be translated into it.AddDataAnnotationsPortableObjectLocalization() (the Localization module registers it).GenerateLocalizationCatalog=false only when intentionally disabling both embedding and updating, not as a fix for extraction errors.references/localization.md — POT configuration and diagnostics, extraction APIs, PO discovery order, contexts, plural forms, content localization internals, startup wiringsrc/docs/reference/modules/Localize/README.md (repo)src/docs/reference/modules/ContentLocalization/README.md (repo)src/docs/reference/modules/DataLocalization/README.md (repo)src/docs/reference/modules/Localize/javascript-localization.md (repo) — IJSLocalizer bridge; no direct JavaScript extractionAGENTS.md (repo root) — build commandsOrchardCMS/OrchardCore
cc8ab81
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.