Logo

dev-resources.site

for different kinds of informations.

Driving Umbraco's dictionary to the edge (of your system)

Published at
12/2/2024
Categories
webdev
umbraco
dotnet
Author
d_inventor
Categories
3 categories in total
webdev
open
umbraco
open
dotnet
open
Author
10 person written this
d_inventor
open
Driving Umbraco's dictionary to the edge (of your system)

Umbraco's translations section allows you to define reusable pieces of flat text. It's an easy way to give a place to small texts and labels that a content editor shouldn't specify manually on every page.
Though it's easy to set up and get started, you may quickly run into challenges when dealing with translated texts in medium size applications. When producing models in the service layer for example, it's not always possible or desirable to consume the UmbracoHelper to access the translations. I've seen some nasty workarounds to access the dictionary and in this post, I would like to propose an alternative approach.

The goal

The goal of this post is to refactor some code that depends on Umbraco's translations in the service layer. At the end, the dependency on Umbraco's translations will have been moved to the razor view. What is important is that the approach is as non-disruptive as possible and easy to read and understand.

ℹ️ Umbraco version
The code in this post is built on Umbraco 15, but also works in Umbraco 13.

The starting point

For the starting point, we'll make a few assumptions:

  • We have a domainmodel for a link with a text and an alt text for screenreaders
  • We have a contentpage that optionally allows you to set the text on the link
  • We have a service that produces the link domainmodel with the text.
  • we have a razor partial view that consumes the link and uses it to display a button.

LinkDomainModel.cs

public record LinkDomainModel(string Url, string Text, string Alt);
Enter fullscreen mode Exit fullscreen mode

MyService.cs

public class MyService(ICultureDictionaryFactory dictionaryFactory)
{
    private ICultureDictionary Dictionary => dictionaryFactory.CreateDictionary();

    public LinkDomainModel GetLinkFromContent(IPublishedContent content)
    {
        var contentToLinkTo = content.Value<IPublishedContent>("linkContent");
        var linkLabel = content.Value<string>("linkLabel");
        if (string.IsNullOrWhiteSpace(linkLabel))
        {
            linkLabel = Dictionary["link label default"];
        }

        var linkAlt = string.Format(Dictionary["link label alttext"], contentToLinkTo.Name);

        return new LinkDomainModel(
            contentToLinkTo.Url(),
            linkLabel,
            linkAlt);
    }
}
Enter fullscreen mode Exit fullscreen mode

ButtonLink.cshtml

@inherits UmbracoViewPage<LinkDomainModel>

<a class="btn btn-primary" href="@Model.Url" alt="@Model.Alt">
    @Model.Text
</a>
Enter fullscreen mode Exit fullscreen mode

The problem with the code

The service class is handling multiple responsibilities. It's responsible for producing link models, but also for translating labels and alttext with Umbraco's translations. This service cannot depend on UmbracoHelper, so it uses a workaround to access the translations. We cannot fix this problem, because the link domainmodel requires us to pass the labels as string: The model suffers from primitive obsession.

The fix

To solve the problem, we need to find a way to transfer the responsibility of translating the text to the consumer of the LinkDomainModel. We do this by removing the dependency on strings in several steps.

Step 1: Introduce a strongly typed model

The first step is to introduce a strongly typed model. Initially, the model will simply mimic the behaviour of a string. That allows us to replace the string values with our model, without disrupting the rest of the system. We'll create a model and update LinkDomainModel:

Label.cs

public record Label(string Value)
{
    // πŸ‘‡ By overriding ToString, our model will render in razor as if it was just a plain string value
    // This way, the existing behaviour inside views is preserved
    public override string? ToString() => Value;

    // πŸ‘‡ By implicitly casting strings to this type,
    // we can pass string values into fields that require type Label
    // This preserves the code inside the service that creates the model
    public static implicit operator Label(string value)
        => new(value);
}
Enter fullscreen mode Exit fullscreen mode

LinkDomainModel.cs

public record LinkDomainModel(string Url, Label Text, Label Alt);
Enter fullscreen mode Exit fullscreen mode

This change may seem insignificant and perhaps even unnecessary, but it gives us the needed advantage for step 2:

Step 2: Separating static and translatable text

Now that we have a model, we can start introducing some polymorphism. This will allow us to make a distinction between static text and translatable texts. We update the Label class as follows:

Label.cs

public abstract record Label()
{
    // πŸ‘‡ This call is changed so it now returns a StaticLabel.
    public static implicit operator Label(string value)
        => Text(value);

    // πŸ‘‡ This static factory abstracts the translatable label,
    // so the rest of the application doesn't need to know that it exists
    public static Label Translate(string key, params object?[] parameters)
        => new TranslatableLabel(key, parameters);

    // πŸ‘‡ This static factory abstracts the static text implementation
    public static Label Text(string value)
        => new StaticLabel(value);
}

// πŸ‘‡ We move the value parameter to the static label
// We also move the ToString override to here, along with the string value
public record StaticLabel(string Value) : Label
{
    public override string ToString() => Value;
}

// πŸ‘‡ We introduce a new type of label that can hold a dictionary key and an optional set of parameters
public record TranslatableLabel(string Key, params object?[] Parameters) : Label;
Enter fullscreen mode Exit fullscreen mode

Still at this point, nothing has changed, but we can now differentiate between static and translatable texts. This allows us to start moving the responsibilities from the service layer to the razor view.

Step 3: Remove the dependency from the service layer

To remove the dependency, we will need to make some disruptive changes in our view. Let's start by changing the service layer:

ExampleService.cs

public static class ExampleService
{
    public static LinkDomainModel GetLinkFromContent(this IPublishedContent content)
    {
        var contentToLinkTo = content.Value<IPublishedContent>("linkContent");
        string? labelString = content.Value<string>("linkLabel");
        Label linkLabel = !string.IsNullOrWhiteSpace(labelString)
            ? Label.Text(labelString)
            : Label.Translate("link label default");

        var linkAlt = Label.Translate("link label alttext", contentToLinkTo.Name);

        return new LinkDomainModel(
            contentToLinkTo.Url(),
            linkLabel,
            linkAlt);
    }
}
Enter fullscreen mode Exit fullscreen mode

By explicitly using the Label class, we were able to completely remove the dependency on the umbraco dictionary. We could even make the class static and turn the method into an extension method.

This change has broken the razor views and we will have to make a change to the razor view to make it work. To fix this, we first make an extension method on UmbracoViewPage<TModel>:

LabelExtensions.cs

public static class LabelExtensions
{
    public static string? ToText<TModel>(this UmbracoViewPage<TModel> page, Label label)
        => label switch
        {
            // πŸ‘‡ Check which type of label we've found and convert it to text accordingly
            TranslatableLabel translatableLabel => page.Translate(translatableLabel),
            StaticLabel staticLabel => staticLabel.Value,
            _ => throw new UnreachableException()
        };

    private static string? Translate<TModel>(this UmbracoViewPage<TModel> page, TranslatableLabel label)
        => (page.Umbraco.GetDictionaryValue(label.Key)?.Trim(), label.Parameters) switch
        {
            (null or "", _) => null,
            (var text, null or []) => text,
            (var text, var parameters) => string.Format(text, parameters)
        };
}
Enter fullscreen mode Exit fullscreen mode

You can choose one of two ways to fix the partial view. You can simply use this method directly inside the view:

ButtonLink.cshtml

@inherits UmbracoViewPage<LinkDomainModel>

<a class="btn btn-primary" href="@Model.Url" alt="@this.ToText(Model.Alt)">
    @this.ToText(Model.Text)
</a>
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can create a new class that inherits from UmbracoViewPage<TModel>:

CustomViewPage.cs

public abstract class CustomViewPage<TModel> : UmbracoViewPage<TModel>
{
    // πŸ‘‡ Whenever we come across one of our labels inside a razor view,
    // we apply the transform before passing it to the base implementation
    public override void Write(object? value)
        => base.Write(value is Label label ? this.ToText(label) : value);
}
Enter fullscreen mode Exit fullscreen mode

If you do this, you can make a more subtle change to your razor view:

ButtonLink.cshtml

@* πŸ‘‡ This page now uses a custom base class *@
@inherits CustomViewPage<LinkDomainModel>

<a class="btn btn-primary" href="@Model.Url" alt="@Model.Alt">
    @Model.Text
</a>
Enter fullscreen mode Exit fullscreen mode

With this CustomViewPage, we can use our labels the same way as we would a simple string, but still automatically translate all dictionary keys with the Umbraco translations. You can't even see in your razor view that you're using Umbraco's translations.

Recap

We started with an application that had a dependency on the Umbraco translations inside the service layer. In a series of steps, we updated our model so that we can move the responsibility of translating keys to the razor view. We ended up with a model that allows us to gradually remove Umbraco's translations from the service layer. You can find a full working example on my GitHub.

Acknowledgement

This post was heavily inspired by Zoran Horvat on youtube. They explain what primitive obsession is and present a similar process to eliminate it from business applications.

Final thoughts

I thought this trick was pretty neat. I see a lot of potential for this approach to translations and I think it's a good solution to isolate the responsibility for translating text inside razor views, where it belongs.

On the other hand, I have also been told that this is excessive and that having Umbraco translations in the service layer isn't really a problem. What do you think of this? I would love to read your thoughts in the comments!

Thank you for reading and I'll see you in my next blog! 😊

umbraco Article's
30 articles in total
Favicon
It doesn’t have to be complicated to build a custom Umbraco property editor
Favicon
please create a post for custom section's tree
Favicon
Umbraco package of the month: xStatic
Favicon
Umbraco and Bellissima: Swagger, Tokens, Entry Points
Favicon
Catching the Bus? How a Service Bus and Azure Functions Can Help Your Integration Reliability
Favicon
Driving Umbraco's dictionary to the edge (of your system)
Favicon
Managing Stale Carts in Umbraco Commerce
Favicon
Quick fix for IPublishedSnapshotAccessor issues when upgrading to Umbraco 15
Favicon
Umbraco package of the month: Auto dictionaries
Favicon
Umbraco package of the month: Uskinned
Favicon
Learning TDD by doing: Tagging members in Umbraco's Rich Text Editor
Favicon
Learning TDD by doing: Dealing with Umbraco's published content
Favicon
My Highlights from the Umbraco US Festival 2024
Favicon
Mastering Authorization in Umbraco 14/15: Real-World Management API Challenges and Solutions
Favicon
Adding Entity Actions to Trees in Umbraco 14
Favicon
Umbraco Forms 12+, Custom Field Type
Favicon
Make your own Umbraco Starter Kit without making a Starter Kit
Favicon
Adding Dynamic Routes to Umbraco 14 Sections
Favicon
Building Custom Trees in Umbraco 14 using menus!
Favicon
Doing custom sections in Umbraco 14
Favicon
Facetted search with Examine - Umbraco 13
Favicon
Server-side registering of Package Manifest in Umbraco 14
Favicon
How to skip login screens during development for Umbraco 13 Users and Members
Favicon
My journey through programmatically creating Umbraco `stuff`
Favicon
Deploying TypeScript Type Definitions for Umbraco v14 Packages
Favicon
Creating an Umbraco Backoffice Accessor for Conditional Page Rendering
Favicon
Create an API in Umbraco in 5 Minutes: A Quick Guide for Developers
Favicon
How to Create a New Umbraco Project: A Step-by-Step Guide
Favicon
Programmatically add a new 'Accepted file extension'
Favicon
Umbraco CodeGarden24

Featured ones: