Skip to main content

Translating HTML Attributes

Some text in Blazor applications is not rendered as normal page content.

For example, text inside HTML attributes such as:

  • placeholder
  • title
  • alt
  • aria-label
  • aria-describedby
  • value

These attributes cannot always be translated with TranslatableText, because they are not rendered as child content.

For these cases, you can use TranslationsCollector directly.

Example: translating an input placeholder

@implements IDisposable

<input class="text-sm border rounded-lg px-3 py-1.5 w-full sm:w-64"
placeholder="@Placeholder" />

@code {
[CascadingParameter]
public TranslationsCollector? Collector { get; set; }

private bool _subscribed;

private string Placeholder =>
Collector?.GetText("Employees.SearchPlaceholder", "Search employees…")
?? "Search employees…";

protected override void OnParametersSet()
{
if (Collector is null)
{
return;
}

Collector.RegisterText("Employees.SearchPlaceholder", "Search employees…");

if (!_subscribed)
{
Collector.TranslationsChanged += OnTranslationsChanged;
_subscribed = true;
}
}

private void OnTranslationsChanged()
=> InvokeAsync(StateHasChanged);

public void Dispose()
{
if (Collector is not null)
{
Collector.TranslationsChanged -= OnTranslationsChanged;
}
}
}

How it works

The component does three things:

  1. Registers the attribute text with TranslationsCollector.
  2. Reads the translated value using Collector.GetText(...).
  3. Re-renders the component when translations change.
Collector.RegisterText("Employees.SearchPlaceholder", "Search employees…");

This tells Translazor that this text should be translated and cached.

Collector.GetText("Employees.SearchPlaceholder", "Search employees…")

This returns the translated value if available. If the translation is missing, Translazor uses the fallback text.

Why StateHasChanged is needed

Attribute values are calculated by Blazor when the component renders.

When the selected language changes, the component needs to re-render so the translated attribute value can be applied.

That is why the component subscribes to:

Collector.TranslationsChanged += OnTranslationsChanged;

And then calls:

InvokeAsync(StateHasChanged);

Always unsubscribe

If your component subscribes to TranslationsChanged, it should also unsubscribe when the component is disposed.

public void Dispose()
{
if (Collector is not null)
{
Collector.TranslationsChanged -= OnTranslationsChanged;
}
}

This avoids memory leaks and prevents disposed components from receiving translation update events.

Translating multiple attributes

You can register and read multiple attributes in the same component.

<input placeholder="@SearchPlaceholder"
title="@SearchTitle"
aria-label="@SearchAriaLabel" />

@code {
[CascadingParameter]
public TranslationsCollector? Collector { get; set; }

private bool _subscribed;

private string SearchPlaceholder =>
Collector?.GetText("Employees.SearchPlaceholder", "Search employees…")
?? "Search employees…";

private string SearchTitle =>
Collector?.GetText("Employees.SearchTitle", "Search employees by name or email")
?? "Search employees by name or email";

private string SearchAriaLabel =>
Collector?.GetText("Employees.SearchAriaLabel", "Search employees")
?? "Search employees";

protected override void OnParametersSet()
{
if (Collector is null)
{
return;
}

Collector.RegisterText("Employees.SearchPlaceholder", "Search employees…");
Collector.RegisterText("Employees.SearchTitle", "Search employees by name or email");
Collector.RegisterText("Employees.SearchAriaLabel", "Search employees");

if (!_subscribed)
{
Collector.TranslationsChanged += OnTranslationsChanged;
_subscribed = true;
}
}

private void OnTranslationsChanged()
=> InvokeAsync(StateHasChanged);

public void Dispose()
{
if (Collector is not null)
{
Collector.TranslationsChanged -= OnTranslationsChanged;
}
}
}

Use clear keys that describe the page and the attribute.

Employees.SearchPlaceholder
Employees.SearchTitle
Employees.SearchAriaLabel
Products.FilterPlaceholder
Contact.EmailPlaceholder
Contact.MessagePlaceholder

A useful pattern is:

PageName.ElementNameAttribute

For example:

Employees.SearchPlaceholder
Employees.SearchAriaLabel
Contact.EmailPlaceholder
Contact.SubmitButtonTitle

When to use this approach

Use TranslationsCollector directly when the text is inside an attribute.

Good examples:

<input placeholder="@Placeholder" />

<img alt="@ImageAlt" />

<button title="@ButtonTitle">
Save
</button>

<input aria-label="@SearchAriaLabel" />

For normal visible text inside the page body, use TranslatableText instead.

<TranslatableText Key="Employees.Title" Text="Employees" />