Integration Guide
Everything you need to fetch, format, and ship translations from WiredStrings into your app.
Getting Started
Set up your project, create a publishable API key, 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
-
A publishable API key (
ws_pk_…), created by an org owner or admin under API keys in the console
Base URL
https://api.wiredstrings.app/v1 Quick Start
Replace YOUR_PUBLISHABLE_KEY with your ws_pk_… key and my-project with your project slug:
curl -H "Authorization: Bearer YOUR_PUBLISHABLE_KEY" \
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 send an API key as a Bearer token in the Authorization header.
curl -H "Authorization: Bearer YOUR_PUBLISHABLE_KEY" \
https://api.wiredstrings.app/v1/translations/my-project/en Key Types
- Publishable (
ws_pk_…): reads translations and languages only. Safe to ship in web and mobile apps — use it for delivery and the SDK. - Secret (
ws_sk_…): the management API (CLI push and export, CI, the Figma plugin). Keep it on servers, in CI secrets and on developer machines.
Scopes and Projects
Every key lists the projects it may reach, or is explicitly granted all projects. A key for "project-a" cannot read translations from "project-b". Secret keys also hold the permission scopes chosen at creation: the console offers the presets CLI sync (pull/push), Content automation (CLI sync plus creating and editing keys and translations) and Custom.
Only organization owners and admins create keys, and a key works only while its creator is still an active owner or admin of the organization.
Token Introspection
Check a key's kind, scopes and the projects it can access:
/v1/token-info curl -H "Authorization: Bearer YOUR_PUBLISHABLE_KEY" \
https://api.wiredstrings.app/v1/token-info Example response
{
"token_id": "tok_abc123",
"token_name": "Production delivery",
"kind": "publishable",
"scopes": ["translation:read", "language:read"],
"all_projects": false,
"projects": [
{ "id": "proj_1", "name": "My Project", "default_locale": "en" }
],
"figma_plugin_enabled": false
} Publishable keys (ws_pk_…) can only read translations, so they may ship in client apps. Never expose a secret key (ws_sk_…) in client-side code.
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_PUBLISHABLE_KEY" \
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_PUBLISHABLE_KEY" \
https://api.wiredstrings.app/v1/translations/my-project/en
# Response includes: ETag: "abc123"
# Subsequent request — send If-None-Match
curl -H "Authorization: Bearer YOUR_PUBLISHABLE_KEY" \
-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_PUBLISHABLE_KEY" \
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_PUBLISHABLE_KEY" \
"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_PUBLISHABLE_KEY" \
"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.
Use a publishable key (ws_pk_…) in client-side code — it can only read translations. Never expose a secret key (ws_sk_…) in the browser.
// npm install @wiredstrings/sdk
import { WiredStringsProvider, useTranslation, T } from '@wiredstrings/sdk/react';
function App() {
return (
<WiredStringsProvider
projectId="proj_abc123"
{/* Publishable key (ws_pk_…) — never ship a secret key (ws_sk_…) client-side */}
apiToken={import.meta.env.VITE_WS_PUBLISHABLE_KEY}
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_PUBLISHABLE_KEY }
);
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,
// Publishable key (ws_pk_…) — never expose a secret key (ws_sk_…) in the browser
apiToken: process.env.WS_PUBLISHABLE_KEY,
},
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 (or call the API directly with a publishable key)
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_SECRET_KEY" \
"https://api.wiredstrings.app/v1/projects/my-project/export?locale=en&format=json" \
-o en.json
# Export as CSV
curl -H "Authorization: Bearer YOUR_SECRET_KEY" \
"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:
# Secret key (ws_sk_…) with the "CLI sync" preset
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:
# Secret key (ws_sk_…) with the "CLI sync" preset — push needs it
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. A publishable key (ws_pk_…) is enough for downloads; the CLI's push needs a secret key (ws_sk_…).