# General Translation Knowledge Base: General Translation vs. nestjs-i18n: which should you use?
URL: https://generaltranslation.com/en-US/kb/general-translation-vs-nestjs-i18n.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: nestjs-i18n resolves each request's language in a NestJS app and renders messages from per-language JSON or YAML catalogs, but it doesn't translate anything. General Translation's CLI translates those catalogs, and its gt-node library translates content that only exists at run time, so you can keep nestjs-i18n and add General Translation.
Category: Internationalization
Last updated: 2026-10-16
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-16

nestjs-i18n is an open-source i18n module for NestJS: it works out each request's language from the query string, a header, a cookie, or `Accept-Language`, and `I18nContext` or `I18nService` returns the message for that language from your catalogs. It renders translations, but it doesn't produce them, so every language's catalog is written and kept current by you, your translators, or a service. General Translation fills that gap in two ways: its CLI translates nestjs-i18n's catalogs, and its `gt-node` library translates content that only exists at run time.

## What nestjs-i18n does well

- **Built for Nest.** One `I18nModule.forRoot()` call, [an `@I18n()` parameter decorator for `I18nContext`, and `I18nContext.current()` in services](https://nestjs-i18n.com/quick-start). It works over HTTP, GraphQL, gRPC, and WebSockets.
- **Language resolvers.** [`QueryResolver`, `HeaderResolver`, `CookieResolver`, and `AcceptLanguageResolver`](https://nestjs-i18n.com/concepts/resolver) run in the order you list them, GraphQL and gRPC resolvers cover those transports, and you can write your own.
- **Simple catalogs.** One folder per language (`src/i18n/en/`, `src/i18n/es/`) with [JSON or YAML files](https://nestjs-i18n.com/concepts/loader), where each file name becomes a namespace (`inbox.title`), plus live reloading in development.
- **Plurals and interpolation.** `{name}` placeholders, `$t()` references to other keys, and [plural objects whose `zero`, `one`, `two`, `few`, `many`, and `other` keys](https://nestjs-i18n.com/guides/plurals) are chosen with `Intl.PluralRules`. An opt-in `useICU` setting, [added in 10.8.0](https://github.com/toonvanstrijp/nestjs-i18n/releases/tag/v10.8.0), formats messages with ICU MessageFormat instead.
- **Type safety.** [`typesOutputPath` generates `I18nTranslations` and `I18nPath` types](https://nestjs-i18n.com/guides/type-safety), so a mistyped key fails to compile, and the `nestjs-i18n check` command exits with an error when a language is missing keys.
- **Validation messages.** `I18nValidationPipe`, `I18nValidationExceptionFilter`, and `i18nValidationMessage()` [translate class-validator errors for DTOs](https://nestjs-i18n.com/guides/dto_validation/global-validation).

It's MIT-licensed, and [version 10.8.5 requires Node.js 22 or later](https://github.com/toonvanstrijp/nestjs-i18n/blob/v10.8.5/package.json).

## What nestjs-i18n leaves to you

Here's an inbox catalog in English, with a plural:

```json title="src/i18n/en/inbox.json"
{
  "title": "Inbox",
  "unread": {
    "one": "You have {count} unread message.",
    "other": "You have {count} unread messages."
  }
}
```

The Spanish folder holds a translated copy of the file, and a controller returns `i18n.t('inbox.unread', { args: { count } })`. There's no German folder. Requests to a minimal Nest 11 app with nestjs-i18n 10.8.5:

```text title="Output"
?lang=es&count=3  "Tienes 3 mensajes sin leer."
?lang=de&count=3  "You have 3 unread messages."
```

German falls back to English because its catalog hasn't been translated. Language detection works, but it can't supply the missing text. That's the work nestjs-i18n leaves to you: producing catalogs and keeping them current each time the English changes.

## How General Translation fits a NestJS app

General Translation's CLI translates nestjs-i18n's catalogs where they are. Point a `json` entry at the language folders:

```json title="gt.config.json"
{
  "defaultLocale": "en",
  "locales": [
    "es",
    "de"
  ],
  "files": {
    "json": {
      "include": [
        "src/i18n/[locale]/*.json"
      ]
    }
  }
}
```

[`npx gt translate`](/docs/cli/reference/commands/translate) reads `src/i18n/en/inbox.json` and writes `src/i18n/es/inbox.json` and `src/i18n/de/inbox.json`, keeping every key. Run it in CI, and entries that haven't changed reuse their earlier translations. nestjs-i18n loads the generated catalogs through its existing loader.

This example uses English, Spanish, and German messages with `one` and `other` forms. Preview the generated messages with counts of 1 and 3, and run `nestjs-i18n check -p src/i18n` to check for missing keys before release.

For content that only exists at run time, such as a teammate's post or a database record, use General Translation's [`gt-node`](/docs/node/quickstart). Initialize it once, then pass nestjs-i18n's resolved language to [`withGT`](/docs/node/reference/functions/with-gt) when calling [`tx`](/docs/node/reference/functions/tx):

```ts title="src/main.ts"
import { NestFactory } from '@nestjs/core';
import { initializeGT } from 'gt-node';
import { AppModule } from './app.module';

// Reads GT_PROJECT_ID and GT_API_KEY (a project key with Runtime translation) from the environment.
initializeGT({ defaultLocale: 'en', locales: ['en', 'es', 'de'] });

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
```

```ts title="src/posts.controller.ts"
import { Controller, Get, Param } from '@nestjs/common';
import { I18n, I18nContext } from 'nestjs-i18n';
import { tx, withGT } from 'gt-node';
import { PostsService } from './posts.service';

@Controller('posts')
export class PostsController {
  constructor(private readonly posts: PostsService) {}

  @Get(':id')
  async findOne(@Param('id') id: string, @I18n() i18n: I18nContext) {
    return withGT(i18n.lang, async () => {
      const post = await this.posts.findOne(id);
      return { ...post, body: await tx(post.body, { $context: 'A post written by a user' }) };
    });
  }
}
```

Both tools now use the language selected by your existing nestjs-i18n resolvers. If `?lang=es` takes priority over `Accept-Language: de`, the catalog messages and the translated post are both Spanish. Use catalogs for fixed interface copy and [`tx`](/docs/node/reference/functions/tx) for content created by your users.

Keep nestjs-i18n for typed keys, catalog lookups, and validation messages. General Translation supplies translation generation, a shared Glossary and Custom Prompts, and [Dashboard review](/docs/platform/dashboard/guides/reviewing-translations). Use the catalog workflow above for fixed copy and `tx` for dynamic content.

## nestjs-i18n vs. General Translation at a glance

| Feature | nestjs-i18n | General Translation |
| --- | --- | --- |
| Role | Resolves the request language and renders messages from catalogs | Translates catalogs, and translates dynamic content at run time |
| Translates your content | No | Yes, with [`npx gt translate`](/docs/cli/reference/commands/translate) and [`tx`](/docs/node/reference/functions/tx) |
| Language detection | Query, header, cookie, `Accept-Language`, GraphQL, gRPC, and custom resolvers | Uses the same resolved language through [`withGT`](/docs/node/reference/functions/with-gt) |
| Plurals | Per-language category keys, chosen with `Intl.PluralRules` | Translates the plural keys your source file has |
| Type safety | Generated key types and a missing-key check | Keeps catalog keys for the same generated types |
| Validation messages | class-validator integration | Translates the catalogs those messages come from |
| Cost | Free and MIT-licensed | [Usage-based](https://generaltranslation.com/pricing) |

## When to use which

Keep nestjs-i18n for its resolvers, typed keys, and validation integration. Add General Translation when you want catalogs translated in CI as the English changes, or when your API returns user or database content that needs translating on the fly. Your existing controllers keep using the same keys; General Translation keeps the text behind those keys current.

## Using both

1. Leave nestjs-i18n and its folders as they are.
2. Add `gt.config.json` with your catalog path and set `GT_API_KEY` and `GT_PROJECT_ID`. (See [Configuring the CLI](/docs/cli/guides/configuring#credentials).)
3. Upload the translations you already have with [`npx gt upload`](/docs/cli/reference/commands/upload), then run [`npx gt translate`](/docs/cli/reference/commands/translate) in CI.
4. For later local translation edits, run [`npx gt save-local`](/docs/cli/reference/commands/save-local) before generating again. Add `gt-node` where you need runtime translation.

## Key takeaways

- nestjs-i18n handles language detection, typed keys, plurals, and validation messages in NestJS, but every catalog in every language is yours to write, including each language's plural forms.
- General Translation's CLI translates nestjs-i18n's JSON catalogs in place, and `gt-node` translates dynamic content using the same request language.
- Use them together: nestjs-i18n renders, General Translation translates.

## Related questions

- [General Translation vs. i18next, react-intl, and next-intl: which i18n library should you use?](https://generaltranslation.com/en-US/kb/general-translation-vs-i18n-libraries.mdx): Open-source libraries such as i18next, react-intl, and next-intl don't translate anything. They display translations you supply in a catalog file for each language, written by you, your translators, or a separate service. General Translation's libraries are open source too and do the same job, and General Translation also does the translating, with your code as context.
- [Why use General Translation instead of a TMS (Smartling, Lokalise, Phrase, Crowdin, or Transifex)?](https://generaltranslation.com/en-US/kb/general-translation-vs-tms.mdx): A translation management system (TMS) such as Smartling, Lokalise, Phrase, Crowdin, or Transifex coordinates translators, but engineers still extract strings, send them out, wait for translations, and wire them back into the product. General Translation translates content in your code with its context, ships every language with each release, and runs on open-source libraries with no lock-in.
- [Should you build your own i18n or use General Translation?](https://generaltranslation.com/en-US/kb/general-translation-vs-in-house-i18n.mdx): Hand-rolled i18n (a dictionary per language and a small t() helper) is fine for one or two languages and a little copy. As an app grows, you rebuild plural rules, fallbacks, and file syncing, and you still need to produce translations. General Translation's open-source libraries handle rendering, and General Translation generates and updates translations from your code.
- [General Translation vs. Symfony Translation: which should you use?](https://generaltranslation.com/en-US/kb/general-translation-vs-symfony-translation.mdx): Symfony Translation renders messages in PHP and Twig. General Translation translates its YAML catalogs in CI, keeps them current as your source changes, and gives reviewers a Dashboard to edit them. Keep Symfony's rendering and add automated translation to your release workflow.
- [Can I do i18n without translation files?](https://generaltranslation.com/en-US/kb/i18n-without-translation-files.mdx): Yes. With gt-react or gt-next, content stays inline in your components inside a `<T>` component, and translations are generated at build time as output, so there are no keys or per-locale JSON files to write and keep in sync.

## Previous and next questions

- Previous question: [General Translation vs. Mobility: which should you use?](https://generaltranslation.com/en-US/kb/general-translation-vs-mobility.mdx)
- Next question: [General Translation vs. Paraglide JS: which should you use?](https://generaltranslation.com/en-US/kb/general-translation-vs-paraglide.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/kb/sitemap.md) for every article.
