npx skills add ...
npx skills add lingui/skills --skill enhanced-message-context
Add translator comments to Lingui messages so translations are accurate. Use when adding or modifying translatable messages, when strings are short or ambiguous ("Back", "Delete", "Post"), when placeholders are unclear ({count}, {name}), when deciding between comment and context, or when auditing extracted .po catalogs for missing translator context.
npx skills add lingui/skills --skill enhanced-message-context
When implementing Lingui i18n, add translator comments to the messages that need them, so translators have the context to choose the right tone, length, and wording. Which messages need them is the whole question, and the tiers below answer it: a short label carries almost no context of its own and a full sentence carries most of it, so they earn very different treatment.
Before writing comments, identify what the product is about — check package.json description, the README, and route/component names. The domain disambiguates terms that are otherwise unresolvable: in a parking app, "Park" is a parking spot, not a nature park; in a social app, "Post" is likely a noun. Reference the domain in comments whenever a term is domain-sensitive.
Prioritize in tiers:
Must comment — translations will be wrong without it:
{count} (count of what?), {name} (user name, file name, project name?)Should comment — quality improves noticeably:
Ship uncommented — the message already carries its own context:
Plural branches don't need separate comments — one comment on the whole message covers all forms.
A catalog where nearly every message carries a comment is evidence the tiers were skipped, not evidence of quality. The must-comment tier at 100% is the target; the whole catalog at 100% is not.
Two costs make that real, and both fall on people rather than tooling: comments on everything train translators to skim past comments, so the load-bearing ones stop being read; and every comment is a claim about the UI that rots when the UI moves.
When auditing an existing catalog, measure the must-comment tier specifically rather than overall #. coverage — short messages with no comment are the finding, and a headline percentage hides them. This lists every uncommented entry with its #: reference, which is enough to sort by tier at a glance:
Paragraph mode (RS='') matches against the whole entry, so leave the patterns unanchored — ^msgid only fires on entries that begin with msgid, and most begin with a #: reference line.
t tagged templates cannot carry a comment of their ownThis is the single most common way a wrapping pass ends up with no context: the tagged-template form has nowhere to put one.
The same applies to msg`…` versus msg({ … }), and to plural(count, { … }) used bare.
Decide the comment while wrapping, not afterwards. For anything in the must-comment tier, reach for the object form the first time. The tagged-template form is the natural thing to type, so leaving it until later turns one decision into a mechanical edit across every attribute, placeholder, and toast in the codebase.
Trans has no such limitation: comment is just a prop, so JSX content can always be commented in place.
A lingui-set directive sets comment for every macro that follows it in the file, tagged templates included. It is the only way to give t`…` a comment without rewriting it:
Reach for it only when a block of messages genuinely shares one description — a toolbar, a table, an empty state. A directive spanning a file of unrelated strings attaches the same words to all of them, which is the saturation the tiers exist to prevent rather than a shortcut around the object form.
Close it with lingui-reset. An open directive keeps applying to messages added to the file later, so its comment ends up describing strings nobody wrote it for, and it changes the IDs extracted for any of them that also inherit a context.
Copy inside components/ui/** — shadcn/ui, or any generator-vendored Radix wrapper — is not yours to comment or wrap. shadcn add overwrites those files wholesale, so a macro or a comment added there disappears on the next update, silently and with a green build.
That governs this skill's audit pass too: when a catalog entry's #: reference points into a vendored directory, record it as a known residual and move to the next entry — the file itself stays closed.
If that copy genuinely needs translating, the fix is a project-owned wrapper component that holds the strings and delegates presentation to the primitive — a deliberate design decision for the project, not something a context pass introduces.
A good translator comment includes:
Location: Where in the UI the message appears
Action/Purpose: What happens or what it means
Disambiguation: Clarify part of speech or meaning
They solve different problems — don't mix them up:
comment is advice for the translator. It never changes the message identity.context changes the message ID: the same text with two context values becomes two catalog entries translated independently. Use it only when the same source text genuinely needs different translations ("right" as direction vs. correctness).context as a namespace (auth.login, settings.title). Identical strings with identical meaning should share one catalog entry; namespacing splits them into duplicate translation work.Lingui provides three ways to add translator comments:
t)For JavaScript code outside JSX:
Trans)For JSX elements:
defineMessage / msg)For messages defined separately from their usage:
Before (no context):
After (with context):
Before (no context):
After (with context):
Before (ambiguous):
After (clarified as verb):
Before (unclear what count represents):
After (clarified):
When implementing or reviewing Lingui messages:
t({ comment, message }), Trans comment=…) at the moment you wrap, and give it location, purpose, and any disambiguationAfter a batch of i18n work, audit the catalog instead of trusting that comments were added along the way:
lingui extract.po file for entries with no #. line (that's where comment lands)#: reference points into a vendored directory (components/ui/**) and record them as known residualslingui extract and confirm the #. lines appearReport the result as the must-comment tier's coverage plus what you deliberately left alone — "every short label commented, 120 self-explanatory sentences left as-is, 25 vendored residuals" — rather than a single catalog-wide percentage, which cannot distinguish those three.
eslint-plugin-lingui 0.16 and later ships a require-comment rule that reports messages with no description, at the source and with a file and line, rather than after extraction. A blank comment is reported too.
The rule is off by default and, as configured out of the box, requires a comment on every message — which is exactly the saturation the tiers argue against. Turning it on globally is not the goal. Two ways it does fit:
warn over a directory to enumerate what is uncommented, triage by tier, then turn it off. Replaces the awk scan above and runs before lingui extract.overrides entry costs little.Two options matter for this skill's advice: allowContext: true accepts a context in place of a comment, and ignorePatterns strips metadata tags such as [CHAR_LIMIT=40] so that a comment carrying only metadata is still reported as missing a description.
A lingui-set comment="…" directive satisfies the rule for the messages it covers. If directives are used that way, enable require-directive-reset alongside it, which reports a lingui-set that is never closed.
<Trans comment="Button in the toolbar that returns to the previous page">Back</Trans>// lingui-set comment="Alt text for the logos in the site header"
<img alt={t`Company logo`} />
<img alt={t`Partner logo`} />
// lingui-resetimport { t } from "@lingui/core/macro";
// With comment
const backLabel = t({
comment: "Button in the navigation bar that returns to the previous page",
message: "Back",
});
// With comment and variable
const uploadSuccess = t({
comment: "Success message showing the name of the file that was uploaded",
message: `File ${fileName} uploaded successfully`,
});import { Trans } from "@lingui/react/macro";
// With comment
<Trans comment="Button that deletes the selected email message">Delete</Trans>
// With comment in a component
<button>
<Trans comment="Label for button that saves changes to user profile">
Save
</Trans>
</button>import { defineMessage } from "@lingui/core/macro";
const messages = {
deleteButton: defineMessage({
comment: "Button that permanently removes the item from the database",
message: "Delete",
}),
statusLabel: defineMessage({
comment: "Shows whether the service is currently operational. Values: 'Active', 'Inactive', 'Pending'",
message: "Status: {status}",
}),
};<button onClick={goBack}>
<Trans>Back</Trans>
</button><button onClick={goBack}>
<Trans comment="Button in the toolbar that navigates to the previous page">
Back
</Trans>
</button>const columns = [
{ key: "name", label: t`Name` },
{ key: "status", label: t`Status` },
];const columns = [
{
key: "name",
label: t({
comment: "Column header in the projects table showing project name",
message: "Name"
})
},
{
key: "status",
label: t({
comment: "Column header showing project status: Active, Inactive, or Archived",
message: "Status"
})
},
{
key: "created",
label: t({
comment: "Column header showing the date when the project was created",
message: "Created"
})
},
];<button onClick={handlePost}>
<Trans>Post</Trans>
</button><button onClick={handlePost}>
<Trans comment="Button that publishes the content. Used as a verb (to post), not a noun (a post)">
Post
</Trans>
</button>const message = t`${count} items selected`;const message = t({
comment: "Shows the number of email messages currently selected in the inbox",
message: `${count} items selected`,
});// Message is clear on its own; adding a comment with location still helps translators
<Trans comment="Validation hint shown below the password field on the sign-up form">
Your password must contain at least 8 characters, including one uppercase letter and one number.
</Trans>#. Button in the toolbar that navigates to the previous page
#: src/components/Toolbar.tsx:24
msgid "Back"
msgstr ""