Guides
Static analysis
eloqnt/cli can read and understand your source code, which unlocks usage-aware lint rules and call-site context for translations (next-intl-only).
To enable static analysis of source code, you can configure srcPath:
.eloqnt/config.ts
export default {srcPath: './src',messages: {path: './messages',locales: 'infer',sourceLocale: 'en',format: 'json'}};
What this gives you
- The
orphan-messagelint rule can detect messages that are no longer used in source code - The
undefined-keylint rule finds keys your code calls that are not present in your messages eloqnt translateandeloqnt reviewwill get additional context on how a message is used in order to improve translation quality
What it asks of your code
Reading source means resolving every t() call to a key without having to actually run the app.
Two rules enable this:
treceives a literal string as its message argument.tis called in the same function body where it came fromuseTranslations,getTranslations,useExtractedorgetExtracted.
Supported:
import {useTranslations} from 'next-intl';function Example({name}) {const t = useTranslations('Example');// ✅ A string literalt('greeting');// ✅ Dynamic values go in the second argument, not the keyt('welcome', {name});function onClick() {// ✅ Event handlers are fine, `t` is still in scopet('clicked');}// ✅ JSX is also finereturn <button onClick={onClick}>{t('submit')}</button>;}
Destructuring from Promise.all is supported too:
import {getTranslations} from 'next-intl/server';async function Async() {// ✅ `t` is still statically analyzableconst [t, post] = await Promise.all([getTranslations(), getPost()]);return <h1>{t('title')}</h1>;}
Not supported:
import {useTranslations} from 'next-intl';function Example({status, items}) {const t = useTranslations();// ❌ Only known at runtimet(status);// ❌ An interpolated key is still a runtime valuet(`Example.status.${status}`);t('Example.status.' + status);// ❌ Handing `t` to another function moves the call out of scoperenderRow(t);}
Translating enum-like values
The common reason to reach for a dynamic key is a value with a small set of cases.
To avoid the dynamic key, you can use a shared hook or async function (in case of getTranslations) to hold the actual call to t:
import {useTranslations} from 'next-intl';type OrderStatus = 'pending' | 'shipped' | 'delivered';function useOrderStatusLabel() {const t = useTranslations('OrderStatus');return function getOrderStatusLabel(status: OrderStatus) {switch (status) {case 'pending':return t('pending');case 'shipped':return t('shipped');case 'delivered':return t('delivered');}};}
function OrderStatus({status}: {status: OrderStatus}) {const getOrderStatusLabel = useOrderStatusLabel();return <span>{getOrderStatusLabel(status)}</span>;}