Integration Guide
Everything you need to fetch, format, and ship translations from WiredStrings into your app.
Getting Started
Set up your project, create an API token, and make your first request.
Prerequisites
- A WiredStrings account with at least one project
- At least one translation key with values in your target language
-
An API token with
translations:readscope
Base URL
https://api.wiredstrings.app/v1 Quick Start
Replace YOUR_API_TOKEN with your token and my-project with your project slug:
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://api.wiredstrings.app/v1/translations/my-project/en Example response
{
"project": "my-project",
"language": "en",
"format": "flat",
"translations": {
"auth.login.title": "Sign In",
"auth.login.button": "Log In",
"dashboard.welcome": "Welcome back, {{name}}"
}
} Authentication
All API requests require a Bearer token in the Authorization header.
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://api.wiredstrings.app/v1/translations/my-project/en Token Scopes
Tokens are scoped to specific projects. A token with access to "project-a" cannot read translations from "project-b".
Token Introspection
Check which projects a token can access:
/v1/token-info curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://api.wiredstrings.app/v1/token-info Example response
{
"token": {
"id": "tok_abc123",
"name": "Production Token",
"scopes": ["translations:read"],
"expires_at": "2027-01-01T00:00:00Z"
},
"projects": [
{ "id": "proj_1", "name": "My Project" }
]
} Never expose API tokens in client-side code. Use a backend proxy or download translations at build time.
Fetch All Translations
/v1/translations/{projectId}/{localeCode} Parameters
| Name | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Your project ID (find it in the project Settings tab) |
localeCode | string | Yes | Language code (e.g., en, de, fr-FR) |
format | string | No | Output format: flat (default), nested, android_xml, ios_strings, ios_xcstrings |
tags | string | No | Comma-separated tag filter (AND logic) |
include_tags | boolean | No | Include tag metadata in response |
resolve_fallbacks | boolean | No | Resolve missing keys from fallback languages (default: true) |
render | boolean | No | Render interpolation variables server-side |
Basic Request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://api.wiredstrings.app/v1/translations/my-project/en ETag Caching
Every response includes an ETag header. Send it back with If-None-Match to get a 304 Not Modified when translations haven't changed.
# First request — save the ETag
curl -i -H "Authorization: Bearer YOUR_API_TOKEN" \
https://api.wiredstrings.app/v1/translations/my-project/en
# Response includes: ETag: "abc123"
# Subsequent request — send If-None-Match
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
-H 'If-None-Match: "abc123"' \
https://api.wiredstrings.app/v1/translations/my-project/en
# Returns 304 Not Modified if translations haven't changed Example response (flat format)
{
"project": "my-project",
"language": "en",
"format": "flat",
"translations": {
"auth.login.title": "Sign In",
"auth.login.button": "Log In",
"auth.login.forgot": "Forgot password?",
"dashboard.welcome": "Welcome back, {{name}}"
}
} Fetch Single Key
/v1/translations/{projectId}/{localeCode}/{key} curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://api.wiredstrings.app/v1/translations/my-project/en/auth.login.title Example response
{
"project": "my-project",
"language": "en",
"key": "auth.login.title",
"value": "Sign In"
} Server-Side Interpolation
Use {{variable}} syntax in your translation values. WiredStrings can render them server-side
when you pass render=true.
Template Value
"dashboard.welcome": "Welcome back, {{name}}! You have {{count}} notifications." Rendered Request
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://api.wiredstrings.app/v1/translations/my-project/en?render=true&name=John&count=5" Rendered response
{
"translations": {
"dashboard.welcome": "Welcome back, John! You have 5 notifications."
}
}
Reserved parameters (format, tags, render, etc.) are not available as variable names.
Responses with render=true are not cached by ETag since the output depends on query parameters.
Output Formats
Control the response shape with the format query parameter. Default is flat. CSV is available via the Export endpoint.
{
"translations": {
"auth.login.title": "Sign In",
"auth.login.button": "Log In",
"auth.login.forgot": "Forgot password?"
}
} {
"translations": {
"auth": {
"login": {
"title": "Sign In",
"button": "Log In",
"forgot": "Forgot password?"
}
}
}
} <?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="auth_login_title">Sign In</string>
<string name="auth_login_button">Log In</string>
<string name="auth_login_forgot">Forgot password?</string>
</resources> "auth.login.title" = "Sign In";
"auth.login.button" = "Log In";
"auth.login.forgot" = "Forgot password?"; {
"sourceLanguage": "en",
"version": "1.0",
"strings": {
"auth.login.title": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Sign In"
}
}
}
}
}
} Fallback Languages
When a translation is missing in the requested language, WiredStrings resolves it from fallback languages automatically.
How It Works
Each language can define a fallback_code. When a key is missing:
-
Check the requested locale (e.g.
en-GB) -
Check the parent locale if set (e.g.
en) - Check the project's default language
Maximum fallback depth is 5 to prevent circular references.
Disable Fallbacks
Return only keys that exist in the requested locale:
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://api.wiredstrings.app/v1/translations/my-project/en-GB?resolve_fallbacks=false" Backend Integration
Full client examples with ETag caching for Go, Node.js, and Python.
Always use ETag caching with If-None-Match to avoid re-downloading unchanged translations.
package i18n
import (
"encoding/json"
"fmt"
"net/http"
"sync"
)
type Client struct {
baseURL string
token string
mu sync.RWMutex
cache map[string]map[string]string // locale -> key -> value
etags map[string]string // locale -> etag
}
func New(baseURL, token string) *Client {
return &Client{
baseURL: baseURL, token: token,
cache: make(map[string]map[string]string),
etags: make(map[string]string),
}
}
func (c *Client) Fetch(project, locale string) (map[string]string, error) {
url := fmt.Sprintf("%s/v1/translations/%s/%s", c.baseURL, project, locale)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+c.token)
c.mu.RLock()
if etag, ok := c.etags[locale]; ok {
req.Header.Set("If-None-Match", etag)
}
c.mu.RUnlock()
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotModified {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cache[locale], nil
}
var result struct { Translations map[string]string `json:"translations"` }
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err }
c.mu.Lock()
c.cache[locale] = result.Translations
c.etags[locale] = resp.Header.Get("ETag")
c.mu.Unlock()
return result.Translations, nil
} interface TranslationCache {
translations: Record<string, string>;
etag: string;
}
class WiredStringsClient {
private baseURL: string;
private token: string;
private cache = new Map<string, TranslationCache>();
constructor(baseURL: string, token: string) {
this.baseURL = baseURL;
this.token = token;
}
async fetch(project: string, locale: string): Promise<Record<string, string>> {
const key = `${project}:${locale}`;
const url = `${this.baseURL}/v1/translations/${project}/${locale}`;
const headers: Record<string, string> = {
Authorization: `Bearer ${this.token}`,
};
const cached = this.cache.get(key);
if (cached) headers['If-None-Match'] = cached.etag;
const resp = await fetch(url, { headers });
if (resp.status === 304 && cached) return cached.translations;
const data = await resp.json();
const etag = resp.headers.get('ETag') ?? '';
this.cache.set(key, { translations: data.translations, etag });
return data.translations;
}
}
// Usage
const client = new WiredStringsClient(
'https://api.wiredstrings.app',
process.env.WIREDSTRINGS_TOKEN!
);
const translations = await client.fetch('my-project', 'en'); import os, requests
class WiredStringsClient:
def __init__(self, base_url: str, token: str):
self.base_url = base_url
self.token = token
self._cache: dict[str, dict[str, str]] = {}
self._etags: dict[str, str] = {}
def fetch(self, project: str, locale: str) -> dict[str, str]:
key = f"{project}:{locale}"
url = f"{self.base_url}/v1/translations/{project}/{locale}"
headers = {"Authorization": f"Bearer {self.token}"}
if key in self._etags:
headers["If-None-Match"] = self._etags[key]
resp = requests.get(url, headers=headers)
if resp.status_code == 304:
return self._cache[key]
resp.raise_for_status()
data = resp.json()
self._cache[key] = data["translations"]
self._etags[key] = resp.headers.get("ETag", "")
return data["translations"]
# Usage
client = WiredStringsClient(
"https://api.wiredstrings.app",
os.environ["WIREDSTRINGS_TOKEN"]
)
translations = client.fetch("my-project", "en") Frontend Integration
Use the SDK for the fastest setup, or integrate manually with React, Next.js, or vanilla JavaScript.
Never expose API tokens in client-side code. Proxy requests through your backend or download translations at build time.
// npm install @wiredstrings/sdk
import { WiredStringsProvider, useTranslation, T } from '@wiredstrings/sdk/react';
function App() {
return (
<WiredStringsProvider
projectId="proj_abc123"
{/* Use a read-only public token — never expose write-scoped tokens client-side */}
apiToken={import.meta.env.VITE_WS_TOKEN}
defaultLocale="en-US"
>
<YourApp />
</WiredStringsProvider>
);
}
function Header() {
const { t, locale, setLocale, isLoading } = useTranslation();
if (isLoading) return <p>Loading…</p>;
return (
<header>
<h1>{t('nav.home')}</h1>
<p>{t('welcome.message', { name: 'Alice' })}</p>
<button onClick={() => setLocale('de-DE')}>Deutsch</button>
</header>
);
}
// Or use the inline component:
// <T k="nav.home" /> // npm install @wiredstrings/sdk
// SSR — pages/index.tsx
import { getServerSideTranslations } from '@wiredstrings/sdk/next';
export const getServerSideProps = async (ctx) => {
const translations = await getServerSideTranslations(
ctx.locale ?? 'en-US',
{ projectId: process.env.WS_PROJECT_ID, apiToken: process.env.WS_API_TOKEN }
);
return { props: { ...translations } };
};
// SSG with ISR — pages/index.tsx
import { getStaticTranslations } from '@wiredstrings/sdk/next';
export const getStaticProps = async (ctx) => {
const translations = await getStaticTranslations(ctx.locale ?? 'en-US', { ... });
return { props: { ...translations }, revalidate: 3600 };
}; // npm install @wiredstrings/sdk i18next react-i18next
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { WiredStringsBackend } from '@wiredstrings/sdk/i18next';
i18n
.use(WiredStringsBackend)
.use(initReactI18next)
.init({
backend: {
projectId: process.env.WS_PROJECT_ID,
// Use server-side env vars only — do not expose write-scoped tokens in the browser
apiToken: process.env.WS_API_TOKEN,
},
lng: 'en-US',
fallbackLng: 'en-US',
interpolation: { escapeValue: false },
}); import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// Custom backend that fetches from your proxy
const WiredStringsBackend = {
type: 'backend' as const,
init() {},
read(language: string, _namespace: string, callback: Function) {
// Proxy through your backend — never expose API tokens client-side
fetch(`/api/translations/${language}`)
.then(res => res.json())
.then(data => callback(null, data.translations))
.catch(err => callback(err, null));
},
};
i18n
.use(WiredStringsBackend)
.use(initReactI18next)
.init({
fallbackLng: 'en',
interpolation: { escapeValue: false },
});
// In your components:
// const { t } = useTranslation();
// t('auth.login.title') → "Sign In" // app/[locale]/layout.tsx
import { getTranslations } from '@/lib/wiredstrings';
export default async function LocaleLayout({
children, params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const translations = await getTranslations(params.locale);
return (
<TranslationProvider translations={translations} locale={params.locale}>
{children}
</TranslationProvider>
);
}
// lib/wiredstrings.ts (runs server-side only)
const cache = new Map<string, { data: Record<string, string>; etag: string }>();
export async function getTranslations(locale: string) {
const headers: Record<string, string> = {
Authorization: `Bearer ${process.env.WIREDSTRINGS_TOKEN}`,
};
const cached = cache.get(locale);
if (cached) headers['If-None-Match'] = cached.etag;
const res = await fetch(
`https://api.wiredstrings.app/v1/translations/my-project/${locale}`,
{ headers, next: { revalidate: 300 } }
);
if (res.status === 304 && cached) return cached.data;
const json = await res.json();
cache.set(locale, { data: json.translations, etag: res.headers.get('ETag') ?? '' });
return json.translations;
} // Fetch translations from your backend proxy
async function loadTranslations(locale) {
const res = await fetch(`/api/translations/${locale}`);
const data = await res.json();
return data.translations;
}
// Simple translation function
let translations = {};
async function initI18n(locale) {
translations = await loadTranslations(locale);
}
function t(key, vars = {}) {
let value = translations[key] || key;
for (const [k, v] of Object.entries(vars)) {
value = value.replace(new RegExp(`{{\\s*${k}\\s*}}`, 'g'), v);
}
return value;
}
// Usage
await initI18n('en');
document.querySelector('h1').textContent = t('dashboard.welcome', { name: 'John' }); Mobile Integration
Download translations in native formats for iOS and Android.
For production apps, prefer downloading translations in your CI/CD pipeline and bundling them with your app.
import Foundation
class WiredStringsClient {
private let baseURL: String
private let token: String
init(baseURL: String, token: String) {
self.baseURL = baseURL
self.token = token
}
/// Download .xcstrings for a locale and save to app bundle
func downloadStrings(project: String, locale: String) async throws -> URL {
let url = URL(string: "\(baseURL)/v1/translations/\(project)/\(locale)?format=ios_xcstrings")!
var request = URLRequest(url: url)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: request)
let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("\(locale).xcstrings")
try data.write(to: fileURL)
return fileURL
}
}
// CI/CD alternative — download at build time:
// curl -H "Authorization: Bearer $TOKEN" \
// "https://api.wiredstrings.app/v1/translations/my-app/en?format=ios_xcstrings" \
// -o en.xcstrings import java.net.HttpURLConnection
import java.net.URL
class WiredStringsClient(
private val baseURL: String,
private val token: String
) {
/**
* Download Android XML strings for a locale.
* In production, prefer downloading in CI/CD and bundling in res/values/.
*/
fun downloadStrings(project: String, locale: String): String {
val url = URL("$baseURL/v1/translations/$project/$locale?format=android_xml")
val conn = url.openConnection() as HttpURLConnection
conn.setRequestProperty("Authorization", "Bearer $token")
return conn.inputStream.bufferedReader().readText()
}
}
// CI/CD — download and save to res/values:
// curl -H "Authorization: Bearer $TOKEN" \
// "https://api.wiredstrings.app/v1/translations/my-app/en?format=android_xml" \
// -o app/src/main/res/values/strings.xml
//
// curl -H "Authorization: Bearer $TOKEN" \
// "https://api.wiredstrings.app/v1/translations/my-app/de?format=android_xml" \
// -o app/src/main/res/values-de/strings.xml Export & Import
Bulk export translations in various formats for offline use or migration.
Using the CLI (recommended)
# One-time setup
wiredstrings init
# Export a single locale to a file
wiredstrings export --locale en-US --format flat_json > en-US.json
wiredstrings export --locale de-DE --format android_xml -o strings.xml
# Pull all configured locales at once
wiredstrings pull
# Push default locale back to WiredStrings
wiredstrings push Requires .wiredstrings.yml — run wiredstrings init to create one.
Or via the API directly:
/v1/projects/{projectId}/export?locale={locale}&format={format} # Export as JSON
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://api.wiredstrings.app/v1/projects/my-project/export?locale=en&format=json" \
-o en.json
# Export as CSV
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://api.wiredstrings.app/v1/projects/my-project/export?locale=en&format=csv" \
-o en.csv CI/CD Integration
Automate translation downloads in your build pipeline.
#!/bin/bash
set -euo pipefail
BASE_URL="https://api.wiredstrings.app/v1"
PROJECT="my-project"
LOCALES=("en" "de" "fr" "es")
OUTPUT_DIR="./src/locales"
mkdir -p "$OUTPUT_DIR"
for locale in "${LOCALES[@]}"; do
echo "Downloading $locale..."
curl -sf -H "Authorization: Bearer $WIREDSTRINGS_TOKEN" \
"$BASE_URL/translations/$PROJECT/$locale?format=nested" \
-o "$OUTPUT_DIR/$locale.json"
done
echo "Done — downloaded ${#LOCALES[@]} locales" # 1. One-time setup (run locally)
# npm install -g @wiredstrings/cli
# wiredstrings init ← creates .wiredstrings.yml
# 2. Sync in CI
name: Sync Translations
on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Pull translations
env:
WIREDSTRINGS_API_TOKEN: ${{ secrets.WIREDSTRINGS_API_TOKEN }}
run: |
npm install -g @wiredstrings/cli
wiredstrings pull --json
- uses: peter-evans/create-pull-request@v7
with:
commit-message: 'chore: update translations'
title: 'Update translations from WiredStrings'
branch: chore/update-translations # Requires the WiredStrings composite action in your repo:
# see: docs/DEVELOPER_INTEGRATIONS.md — GitHub Action section
name: Sync Translations
on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
inputs:
command:
type: choice
options: [pull, push]
default: pull
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/translations
with:
api-token: ${{ secrets.WIREDSTRINGS_API_TOKEN }}
command: ${{ github.event.inputs.command || 'pull' }}
- uses: peter-evans/create-pull-request@v7
with:
commit-message: 'chore: update translations from WiredStrings'
title: 'Update translations from WiredStrings'
branch: chore/update-translations name: Download Translations
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * *' # daily at 6 AM
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download translations
env:
WIREDSTRINGS_TOKEN: ${{ secrets.WIREDSTRINGS_TOKEN }}
run: bash scripts/download-translations.sh
- name: Commit if changed
run: |
git diff --quiet src/locales/ && exit 0
git config user.name "github-actions"
git config user.email "actions@github.com"
git add src/locales/
git commit -m "chore: update translations"
git push version: 2.1
jobs:
sync-translations:
docker:
- image: cimg/base:current
steps:
- checkout
- run:
name: Download translations
command: bash scripts/download-translations.sh
- run:
name: Commit if changed
command: |
git diff --quiet src/locales/ && exit 0
git config user.name "circleci"
git config user.email "ci@circleci.com"
git add src/locales/
git commit -m "chore: update translations"
git push
workflows:
nightly-sync:
triggers:
- schedule:
cron: "0 6 * * *"
filters:
branches:
only: main
jobs:
- sync-translations:
context: wiredstrings // Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
triggers { cron('0 6 * * *') }
environment {
WIREDSTRINGS_TOKEN = credentials('wiredstrings-token')
}
stages {
stage('Download Translations') {
steps { sh 'bash scripts/download-translations.sh' }
}
stage('Commit Changes') {
steps {
sh """
git diff --quiet src/locales/ && exit 0
git config user.name "jenkins"
git config user.email "ci@jenkins"
git add src/locales/
git commit -m "chore: update translations"
git push
"""
}
}
}
} Downloading translations at build time means your app doesn't need runtime API access, and your API tokens stay out of client-side bundles.