Why in-app help needs stable identifiers

· Robbert

Every system that serves text to an application has to answer one question first: how does the application ask for the text it needs?

There are three common answers, and the difference between them decides how much work a change costs you two years from now.

By position

The application asks for "the third item in the onboarding list", or "the paragraph under the heading". The text is identified by where it sits.

This works until someone reorders the list. Then every reference is wrong, silently, and the only way to find out is to look at every screen.

By content

The application asks for the text that says "Your payment could not be processed". The string itself is the identifier, which is how most translation setups start.

This works until someone improves the sentence. The improved sentence is a different identifier, so either the change breaks the lookup or you keep the old wording forever as a key and accept that your keys are a museum of sentences you no longer use.

By stable identifier

The application asks for billing.payment-failed. That name says what the text is about rather than what it says or where it sits, so both can change without anything breaking.

This is the only one of the three that survives a rewrite, and it is worth being precise about why: the identifier is a promise between two parties. Your application promises to ask for this name. Whoever owns the content promises that this name will always answer with an explanation of that thing.

What makes an identifier good

It describes the subject, not the wording. settings.api-keys stays correct after five rewrites. settings.create-a-key-button-tooltip does not survive the day the button moves.

It is readable in code. Someone reviewing a pull request should be able to tell what text will appear without looking it up. That rules out numbers and hashes.

It groups. A prefix per area gives you a map for free. When everything in billing. is listed together, a missing explanation is visible.

It never gets reused for something else. A retired key is retired. Pointing an old name at new content is how a tooltip ends up explaining the wrong feature on a screen nobody tested.

The property that matters most

One name, one answer.

If two pieces of content can carry the same identifier, then every request has a hidden question attached: which one? Some systems resolve that by priority, some by most recently updated, and all of them are guessing on your behalf.

The alternative is to make it impossible. An address belongs to one thing, and a second claim on it is refused when someone tries to publish, which is the moment a person is there to fix it. That turns an entire class of runtime surprises into an error message at the only time it is cheap.

One thing that rule does not mean: that the answer is always the same text. The identity is stable, the wording is not. The same key answers with a rewritten sentence tomorrow, and it answers in another language when the request asks for one, because the language is part of the question and not part of the name. What stays fixed is which thing the answer is about.

What you get once identifiers are stable

Rewriting becomes free. The sentence can be replaced, split, translated or shortened, and the application never notices because it asks for the same name.

Moving becomes possible. If the text has an address, the same explanation can appear in a tooltip, an onboarding step and a support screen without existing three times.

And removal becomes visible. If the content behind an address disappears, the application asks for something that is not there, and it can decide what to do. A missing explanation is a known state, not an empty box.

Where to start

Name the first ten. Use the area of your product as the prefix and the subject as the rest, write them down before you write any text, and treat them the way you treat a public API: easy to add to, painful to rename.

That last part is the point. An identifier that everyone is willing to change is not stable, and a stable identifier is the whole reason this works.

Blog