lin is a CLI tool that translates locale JSONs using LLMs
Check out the docs or continue reading below.
npm i -D @rttnd/linor use -g to install globally for non-npm projects.
You will need:
- a project with i18n set up
- a default locale JSON file (e.g.
en-US.json) - API keys for your chosen LLM providers in your .env file (e.g.,
OPENAI_API_KEY,ANTHROPIC_API_KEY)
See LLM Config.
lin will try to automatically detect your i18n configuration from your existing project setup. It supports:
- i18next (
i18next-parser.config.js) - Next.js (
next.config.js) - Nuxt.js (
nuxt.config.js) - Vue I18n (
vue.config.js) - Angular (
angular.json) - Svelte (
svelte.config.js):⚠️ There were issues with parsing svelte files, please setintegration: 'svelte'in your lin config to use a custom parser. - Ember.js (
ember-cli-build.js) - Gatsby (
gatsby-config.js) - Solid.js (
vite.config.js) - Qwik (
vite.config.jsorpackage.json) - Astro (
astro.config.mjsorastro-i18next.config.mjs) - Remix (
package.json)
If your setup is not detected automatically, you can specify the integration using the integration config, and lin will only try to load the specified framework.
Or you can create a configuration file to tell lin about your i18n setup. You have two options:
-
Use
lin.config.ts: Add ani18nobject to your mainlin.config.tsfile.Example
lin.config.ts:import { defineConfig } from '@rttnd/lin' export default defineConfig({ i18n: { locales: ['en-US', 'es-ES'], defaultLocale: 'en-US', directory: 'locales', }, // ... other lin config })
-
Use
i18n.config.ts: Or if you don't plan to use otherlinconfig, just create ai18n.config.tsfile.Example
i18n.config.ts:import { defineI18nConfig } from '@rttnd/lin' export default defineI18nConfig({ locales: ['en-US', 'es-ES'], defaultLocale: 'en-US', directory: 'locales', })
You can use lin in the terminal, in GitHub Actions, or programmatically.
Tip
Run lin -h and lin <command> -h to see all the options.
The main command is translate. It automates the entire process of finding new keys in your code, adding them to your default locale, and translating them into all other languages.
For it to work, you need to provide a default value when you use your translation function:
t('header.title', 'Default value')Then, just run:
lin translatelin will find header.title, add it to your default locale file with the value "Default value", and then translate it to all other locales.
Use the with option to manage the LLM's context.
To translate only specific locales, list them like this:
lin translate es frTo also remove unused keys from all locales, use the --prune flag:
lin translate -uTo make the output more minimal (for CI or scripts), use the --silent flag:
lin translate -SYou can use translate in GitHub Actions. lin will automatically find new keys, add them to your locales, and translate them on every push to main.
Here's an example workflow:
# .github/workflows/lin.yml
name: Lin Translate
on:
push:
branches:
- main
jobs:
translate:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: checkout repo
uses: actions/checkout@v4
- name: setup bun
uses: oven-sh/setup-bun@v2
- name: install deps
run: bun install
- name: lin translate
run: bunx lin translate -S
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Add other provider API keys as needed
# GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
# GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
# CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
- name: commit and push changes
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add locales/
if ! git diff --staged --quiet; then
git commit -m "i18n: auto-translate locales"
git push
fiDon't forget to add your LLM provider API keys to your repo secrets.
While translate is the most end-to-end command, lin provides more granular commands for specific tasks:
translate:check --fix+syncsync: Translates missing keys from your default locale to all other locales using LLMs.add: Adds a new key to your default locale and translates it to all other locales using LLMs.edit: Edit an existing key and its translations manually.del: Remove one or more keys.check: Validate locale files, check for missing/unused keys, or sort them. Quick config check.models: List available LLM models.undo: Revert the last change made bytranslate,sync,add,del,edit, orcheck.
The sync command syncs all locale JSON files with the default locale JSON file. It finds the missing keys in locales, and translates them.
lin syncTo sync only specific locales, list them like this:
lin sync es frYou can also use the sync command to add a new language.
- First add the locale code to
localesin the i18n config - Then run
lin syncand it will create the new locale JSON file
Note
There is some syntax around locale codes:
- Locale JSON file names must match the codes in your
localesconfiguration (e.g.,en-US.jsonfor an'en-US'entry). - Short codes like
'en'also work (e.g.,'en.json'), but these also function as shorthands:lin sync enwill match all locales starting withen-(likeen-USanden-GB). allis a special keyword that matches all localesdefmeans the default locale from the config
add can be useful when writing a new part of the UI. You can use it to quickly add a new key to the default locale and translate it to all the other locales.
lin add ui.button.save Text of the save buttonui.button.save will be the key, and Text of the save button will be the value for the default locale. This will then be translated to all other locales.
Note
if the key is nested, it should be in dot notation like ui.button.save
To add a key to only specific locales, use the -l flag. You can repeat the flag for multiple locales.
lin add -l es -l fr -l def ui.button.save Text of the save buttonThis will add the key to es and fr locales (and the default locale).
Tip
The add, edit, and del commands support key suggestions. If you're not sure about a key, try one of these:
- End your key with a dot to see all available sub-keys (e.g.,
lin del ui.button.). - Type the beginning of a key to get suggestions for matching keys (e.g.,
lin edit ui.but).
lin add ui.b will show suggestions, but if you really want to add an empty key, use an empty string: lin add ui.b ""
edit can be used to quickly edit an existing key in the default locale and all the other locales.
This is a niche command, but maybe useful for quickly editing a specific key without having to search for it, or for LLM agents if you don't want to feed the entire locale json file, or have them edit the files themselves.
lin edit ui.button.save Text of the save buttonTo edit a key in only specific locales, use the -l flag.
lin edit -l en ui.button.save Text of the save buttondel removes keys from the locale JSON files.
lin del nav.title footer.descriptionThe check command is a versatile tool for validating and maintaining locale files.
It's ideal for running in pre-commit hooks or in CI.
By default, it lints your codebase for missing and unused translation keys by comparing your source files against the default locale.
lin checkTo get a minimal output, use the --silent or -S flag. This is recommended for CI and git hooks. See check with git hooks.
lin check -SThis will report any discrepancies. To add the missing keys to your default locale file with empty strings instead of throwing an error, use the --fix flag:
lin check -fTo remove unused keys from all locale files, use the --prune flag.
lin check -uYou can also use check to find missing keys in your locales compared to the default locale file with the --keys flag (this skips the codebase parsing):
lin check -kThis will report any discrepancies. If you want to automatically add the missing keys with empty strings, use the --fix flag:
lin check -k -fYou can also use check to sort your locale JSON files, either alphabetically or based on the key order in your default locale file.
lin check -s abc # sort alphabetically
lin check -s def # sort by default localeTo display detailed info about your config and locales, use the --info flag:
lin check -iA great way to enforce i18n consistency is to run lin check automatically before each commit. You can use simple-git-hooks with lint-staged to set this up easily.
Add this to your package.json:
{
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"{src/**/*.{js,jsx,ts,tsx,vue,svelte,astro},locales/**/*.json}": "lin check -S"
}
}Then run:
npm i -D lint-staged simple-git-hooks
# activate hooks
npx simple-git-hooksYou can also run lin check -S -f or lin check -S -u to automatically fix issues, or even lin translate -S to translate them too.
undo reverts the last changes made by translate, sync, add, del, edit, or check.
lin undoTo see a list of all available LLM providers and models:
- Run
lin -M,lin --modelsorlin modelsto list all models. - To filter by provider, just specify providers after the command:
lin -M openai google
You can also use lin as a library and run commands programmatically. This is useful for integrating lin into your own scripts or tools.
All commands and types are exported from the main package entry point.
The run function allows you to execute any command:
import { run } from '@rttnd/lin'
// Simple usage: run the 'translate' command with the --silent flag
await run('translate', ['-S'])The run function accepts an optional third argument, options, to control execution behavior. It returns a RunResult object with details about the execution.
function run(
command: keyof Commands,
rawArgs?: string[],
options?: {
dry?: boolean // default: false
output?: 'live' | 'capture' | 'silent' // default: 'live'
}
): Promise<{
result: unknown
output?: string
writes?: Record<string, string>
deletes?: string[]
}>dry: A boolean that, iftrue, prevents any file system modifications. Instead of writing or deleting files,runwill return the planned changes in thewritesanddeletesobjects.output: can return the console output inoutput'live'(default) – print only.'capture'– print and return captured text.'silent'– capture text without printing.
Example: Dry Run with Output Capture
Here's how you can see what files check --fix would change, without actually modifying your disk:
import { run } from '@rttnd/lin'
const { output, writes } = await run(
'check',
['--fix'],
{ dry: true, output: 'silent' },
)
console.log('--- Captured Console Output ---')
console.log(output)
console.log('\n--- Files to be Written ---')
for (const [file, content] of Object.entries(writes || {})) {
console.log(`File: ${file}`)
console.log(content) // content is the full file content
}Tip
All properties in the config can be used as CLI flags too.
lin automatically saves a backup of any files modified by the add, del, check, and translate commands. You can disable this feature with the --no-undo flag, or by setting undo: false in your config file.
Important
Otherwise, add the .lin directory to your .gitignore file.
lin uses unconfig to find and load your configuration files. You can use one of the following:
lin.config.ts(or.js,.mjs, etc.).linrc(without extension or.json)linproperty inpackage.jsonIf you are not using one of the auto-detected frameworks, you can put your i18n config inside yourlinconfig, or create a separatei18n.config.tsfile.
See src/config/i18n.ts for a full list of configuration sources.
for the add and sync commands
lin uses the Vercel AI SDK to support multiple LLM providers. The currently supported providers are:
openaianthropicgooglexaimistralgroqazure
You need to specify the model and the provider in your configuration or via the --model (-m) and --provider (-p) CLI flags.
Make sure the corresponding API key is set in your env variables (e.g., OPENAI_API_KEY, ANTHROPIC_API_KEY).
Example lin.config.ts with LLM options:
import { defineConfig } from '@rttnd/lin'
export default defineConfig({
options: {
provider: 'openai',
model: 'gpt-4.1-mini',
temperature: 0,
}
})All options under options are passed to the Vercel AI SDK.
To save LLM options, you can define and name different model configurations in your lin.config.ts file.
// lin.config.ts
export default defineConfig({
options: {
provider: 'openai',
model: 'gpt-4.1-mini',
},
presets: {
'creative-claude': {
provider: 'anthropic',
model: 'claude-sonnet-4-0',
temperature: 0.6,
context: 'Use a playful tone.'
},
'fast-deepseek': {
provider: 'groq',
model: 'deepseek-r1-distill-llama-70b',
},
}
})You can then activate a preset using the --model flag. Any other CLI flags will override the preset's values.
# Use the 'creative-claude' preset
lin sync -m creative-claude
# Use the 'fast-deepseek' preset, but override the temperature
lin add ui.new.feature A new feature -m fast-deepseek -t 0This simple string is directly added to the system prompt. Use it to provide extra information to the LLM about your project.
The batchSize option controls how many target locale files are sent to the LLM for translation in a single request. This can be useful for projects with many languages.
You can set this in your lin.config.ts using batchSize or use the --batchSize (or -b) flag in the CLI. The CLI flag will always override the config file setting.
The with option allows you to control which locale files are included in the LLM's context window. This can significantly improve translation quality by providing the model with more context about your project's wording and style.
You can set this in your lin.config.ts using with or use the --with (or -w) flag in the CLI. The CLI flag will always override the config file setting.
Context Profiles:
none(default): Only the keys to be translated are sent to the LLM. This is the most cost-effective option.def: Includes the entire default locale JSON file (e.g.,en-US.json) in the context.tgt: Includes the full JSON of each locale currently being translated.both: Includes both the default locale file and the target locale files.all: Includes every locale JSON file in the context. This may be expensive.<locale>: You can also provide one or more specific locale codes (e.g.,es-ES,fr).
Examples:
// lin.config.ts
export default defineConfig({
with: 'tgt',
})# Override config and use 'both' profile for this command
lin sync -w both
# Provide specific locales for context
lin add ui.new.key New Key -w es-ES -w fr-FR
# Force no additional context, overriding any config
lin sync -w none