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
Valueis 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 andRes.Keyreturns the key name (SupplierContractOtherSupplierinstead 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
Reslists every key name, scoped included. Resolution is owner-first. IfRes.Keyexisted only for unscoped keys, a developer who wanted the typed form overGetResource("…")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 oneRes.Titlethat 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
RUandARare 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#
- Metadata captions and translations — the same problem for object and field names, which are captions rather than resources.
- In-app help — the ⓘ texts, a third kind of translated string with its own storage.