Integrations
Web Framework Integrations
langctl writes flat (json) or nested (nested-json) JSON files that work with i18next, vue-i18n, ngx-translate and most web frameworks.
React with i18next
npm install i18next react-i18next
langctl init --project my-app --format json --output "public/locales/{lang}.json"
langctl pull // src/i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: false,
interpolation: {
escapeValue: false
},
backend: {
loadPath: '/locales/{{lng}}.json'
}
});
export default i18n; // src/App.tsx
import { useTranslation } from 'react-i18next';
import './i18n';
function App() {
const { t, i18n } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('home.welcome', { username: 'John' })}</h1>
<p>{t('home.subtitle')}</p>
<button onClick={() => changeLanguage('es')}>
Español
</button>
</div>
);
} Vue.js
vue-i18n resolves dotted keys as paths, so pull nested JSON:
npm install vue-i18n
langctl pull my-app -f nested-json -o "src/locales/{lang}.json" -l en,es // src/i18n.ts
import { createI18n } from 'vue-i18n';
import en from './locales/en.json';
import es from './locales/es.json';
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: { en, es }
});
export default i18n;
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import i18n from './i18n';
createApp(App).use(i18n).mount('#app'); <!-- Component.vue -->
<template>
<div>
<h1>{{ $t('home.welcome', { username: 'John' }) }}</h1>
<p>{{ $t('home.subtitle') }}</p>
<button @click="changeLocale('es')">Español</button>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
const { locale } = useI18n();
const changeLocale = (lang) => locale.value = lang;
</script> Angular (ngx-translate)
npm install @ngx-translate/core @ngx-translate/http-loader
langctl pull my-app -f json -o "src/assets/i18n/{lang}.json" -l en,es // app.module.ts
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
@NgModule({
imports: [
HttpClientModule,
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
]
})
export class AppModule { } // component.ts
import { TranslateService } from '@ngx-translate/core';
export class AppComponent {
constructor(private translate: TranslateService) {
translate.setDefaultLang('en');
translate.use('en');
}
switchLanguage(lang: string) {
this.translate.use(lang);
}
}
// component.html
<h1>{{ 'home.welcome' | translate: {username: 'John'} }}</h1>
<p>{{ 'home.subtitle' | translate }}</p> Next.js
next-i18next expects one folder per language with a file per namespace:
npm install next-i18next react-i18next i18next
langctl pull my-app -f json -o "public/locales/{lang}/common.json" -l en,es // next-i18next.config.js
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr'],
localeDetection: true
},
localePath: './public/locales'
};
// next.config.js
const { i18n } = require('./next-i18next.config');
module.exports = {
i18n,
}; // pages/index.tsx
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
export default function Home() {
const { t } = useTranslation('common');
return (
<div>
<h1>{t('home.welcome')}</h1>
<p>{t('home.subtitle')}</p>
</div>
);
}
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common']))
}
};
} Svelte
npm install svelte-i18n
langctl pull my-app -f nested-json -o "src/locales/{lang}.json" -l en,es // src/i18n.js
import { register, init } from 'svelte-i18n';
register('en', () => import('./locales/en.json'));
register('es', () => import('./locales/es.json'));
init({
fallbackLocale: 'en',
initialLocale: 'en'
});
// App.svelte
<script>
import { _ } from 'svelte-i18n';
import './i18n';
</script>
<h1>{$_('home.welcome', { values: { username: 'John' } })}</h1>
<p>{$_('home.subtitle')}</p> Build Scripts
With langctl.json committed, langctl pull needs no arguments:
// package.json
{
"scripts": {
"prebuild": "npm run fetch-translations",
"build": "vite build",
"dev": "npm run fetch-translations && vite",
"fetch-translations": "langctl pull"
}
} CI/CD
name: Build with Translations
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- name: Install Dependencies
run: npm ci
- name: Pull Translations
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: npx langctl@0 pull # project, format and path come from langctl.json
- name: Build
run: npm run build
- name: Deploy
run: npm run deploy Tip
Store the LANGCTL_API_KEY in your repository secrets for secure CI/CD integration. No login step is needed — the CLI reads the key from the environment. To fail a pull request when committed files are out of date instead, use langctl pull --check.
Next Steps
- CI/CD Integration - Automate translation workflows
- File Formats - Flat vs nested JSON, default paths
- Version Control - Git integration
- Dashboard Export - Export from web interface