ZuloOne
English
Sign in
Menu

Text a script shows: resources, not literals

Updated

Every string a script puts in front of a person — a refusal, a log line a user reads, a generated document title — is a resource: a named row with a value per language, owned by the script that shows it. Short recipe: Add a script resource. A literal in C# is text one language can read and nobody can change without a developer.

The rule#

  • User-facing text is a resource. GetResource("NoCustomer"), or typed, Res.NoCustomer.
  • The key belongs to the script that shows it. Create it on that script's Resources tab, not on Developer → Resources.
  • Fill the other languages when you create the key. A key with only Value is a key that speaks English to everyone; it is not a translation that comes later, it is a decision not to translate.

Text nobody reads — an internal cache key, a register analytic name, a subtype value — is not a resource. Those are identifiers and belong in the code, where a typo is a compile error.

Owned and shared#

A resource row either belongs to one script (ScriptMetaId) or to none.

Owned Shared (unscoped)
Created on the script's Resources tab Developer → Resources
Visible to that script every script
Typed Res.Key yes yes
Two scripts, same key name fine, each reads its own one row, one value

Resolution is owner-first: ResourceStore.Get(key) looks for the running script's row, then the unscoped row, then returns the key itself. So a shared key is a fallback, not a shortcut.

Warning

A service is not the running script. GetService<IFoo>() runs the service's method inside its caller — the event handler, command or test — so a key bound to the service's own script is not found and Res.Key returns the key name (SupplierContractOtherSupplier instead of the sentence). Text a service returns to whoever calls it is shared: leave the resource unscoped.

Moving a key that is already shared: open it on the owning script's Resources tab and tick Bind this key to this script. Editing a shared row otherwise leaves it shared on purpose — adopting it takes the key away from every other script that was reading it, and that belongs to a deliberate tick rather than to pressing Save.

Note

The generated Res lists every key name, scoped included. Resolution is owner-first. If Res.Key existed only for unscoped keys, a developer who wanted the typed form over GetResource("…") would have exactly one way to get it: leave the key global. Then every script's Resources tab would show every other script's keys — a job task displaying eight keys that belong to a different job task. That is why stands fill up with shared resources when keys are left unscoped.

When a key reaches the code#

If you change What is needed
the TEXT of an existing key nothing — running scripts see it at once
a NEW key a compile, because Res is generated

That asymmetry is deliberate: the values are read live from the resource store, so an analyst can fix a wording on a running stand, while the NAMES are compiled so a typo is caught rather than silently returning the key.

What it looks like#

public partial class CreateInvoiceTask : TaskScriptBase
{
    public override async Task ExecuteAsync(TaskContext context)
    {
        var customer = await FindCustomerAsync(context);
        if (customer is null)
            throw new InvalidOperationException(Res.InvoiceNoCustomer);

        context.Log(GetResource("InvoiceCreated", culture: "ru"));
    }
}

InvoiceNoCustomer and InvoiceCreated are rows on this script, each with Value, RU and AR. GetResource takes an explicit culture when the text must be in the reader's language rather than the current user's — a document sent to a customer, not a message shown to the operator.

What's not there yet#

  • No per-script namespace in Res. Keys share one flat class, so two scripts owning the same name produce one Res.Title that resolves per script at run time. That is correct but it means IntelliSense lists keys that belong to other scripts.
  • No warning for a key with no translations. Nothing stops a resource whose RU and AR are empty, and nothing reports how many there are.
  • No check that a literal should have been a resource. This page is the rule; no gate enforces it.

Where next#

Was this page helpful?