Journey. As a developer, I want my app to translate user text between
languages — with the right domain vocabulary and tone — and I want a
bilingual chat feature where I configure the pair once and the service
figures out which way to translate on every message.
Linga (:40065, prefix /v3/translation) is a thin, prompt-engineered front end to Evergreen AI. It holds no translation models of its own: each call opens a single-turn Evergreen session (Llama 3.2, text-only), sends a carefully constructed translator system prompt plus your text, and parses the structured JSON the model is asked to return. That means translation quality follows the models Evergreen serves, and Linga calls inherit Evergreen's availability.
There are exactly two REST endpoints:
| Endpoint | Purpose |
|---|---|
POST /v3/translation/translate | One-shot translation with explicit source/target (or auto-detected source) |
POST /v3/translation/translate-bidirectional | A configured language pair that detects the input's language and flips direction automatically |
Both authenticate with the *tenant key pair and* a user JWT** (x-api-key, x-client-app-key, Authorization: Bearer …). The JWT is forwarded to Evergreen AI for the backend session — in non-sandbox environments it must be a valid platform JWT (the QA-login flow below provides one); it is not optional even though Linga itself is only proxying.
Prerequisites
- Auth Service (for a user JWT) and Evergreen AI running and reachable; the
QA tenant's key pair in INAYA_API_KEY / INAYA_APP_KEY.
- An Evergreen model capable of the requested language pair. Linga demands
Llama 3.2 (text-only) — if Evergreen has no capacity for it, calls fail with 500 after an internal retry window (~75 s).
- Linga's OpenAPI spec (
GET /v3/translation/api-docs/openapi.json) is the
discovery surface for the jargon / vernacular enums — there is no language-list endpoint.
Concepts
LanguageDescription— every language slot is an object of up to three
optional fields, all snake_case: locale (BCP-47, e.g. "es-ES"), jargon (domain vocabulary; 15 PascalCase values such as Agriculture, Legal, Healthcare, LayPerson), and vernacular (generational tone; 7 values such as GenerationZ, Millennials, Neutral). Any subset composes: {"locale": "es-ES", "jargon": "Agriculture"} means European Spanish using farming vocabulary. An entirely empty {} means auto-detect (valid for source; for target it is rejected).
- *Response echoes the configured* languages, not the detected ones.** In
translate, source_language / target_language repeat your configuration verbatim (jargon included, e.g. "es-ES, Agriculture") — with auto-detect source, source_language comes back empty. Use translate-bidirectional when you need the model's detected locale: it returns a separate detected_source_locale (BCP-47) plus human-readable source_language / target_language names.
- Errors are plain text, not JSON. Business-rule failures (
400) return
a bare text body (Invalid request: No target language specified); the 401 auth failure returns a small JSON error object; 422 (DetectedLanguageNotInPair) is a best-effort prompt contract — the model is instructed to reply LANGUAGE_NOT_IN_PAIR for out-of-pair input, but compliance is model-dependent, so don't build hard control flow on it.
- Translation is per-call and stateless. There is no glossary, memory, or
feedback loop; consistency across calls comes from the system prompt, not from retained state.
Step 1 — Get a user JWT
Both endpoints require the tenant key pair and a user JWT (forwarded to Evergreen). Mint one via the Auth Service QA login:
USER_TOKEN=$(curl -sf -X POST "${AUTH_URL}/v3/auth/qa/login" \
-H "Content-Type: application/json" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-d "{\"device_id\": \"g60-demo\", \"locale\": \"en-US\", \"tenant_id\": \"${TENANT_ID}\", \"username\": \"g60-translator\"}" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
test -n "${USER_TOKEN}" && echo "JWT acquired (${#USER_TOKEN} chars)"Expected — a long PS512-signed JWT; qa/login requires device_id (min 8 chars), locale, tenant_id, and username.
Step 2 — One-shot translation with explicit locales
curl -s -X POST "${LIN_URL}/v3/translation/translate" \
-H "Content-Type: application/json" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Authorization: Bearer ${USER_TOKEN}" \
-d '{"source": {"locale": "en-US"}, "target": {"locale": "es-ES"}, "text": "The chickens escaped again."}' \
| python3 -c '
import json, sys
r = json.load(sys.stdin)
assert r["original_text"] == "The chickens escaped again."
assert r["source_language"] == "en-US" and r["target_language"] == "es-ES"
assert len(r["translated_text"]) > 0
print("translated:", r["translated_text"])
'Expected response — 200 with snake_case fields: {"original_text", "translated_text", "source_language": "en-US", "target_language": "es-ES"}. text accepts 1–10,000,000 characters; an empty target is rejected.
Step 3 — Auto-detect the source language
Send an empty source {} and Linga asks the model to identify the language:
curl -s -X POST "${LIN_URL}/v3/translation/translate" \
-H "Content-Type: application/json" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Authorization: Bearer ${USER_TOKEN}" \
-d '{"source": {}, "target": {"locale": "en-US"}, "text": "Buenos días, ¿cómo estás?"}' \
| python3 -c '
import json, sys
r = json.load(sys.stdin)
assert r["target_language"] == "en-US"
assert "morning" in r["translated_text"].lower() or "how are you" in r["translated_text"].lower()
print("auto-detected translation:", r["translated_text"])
print("note: source_language echoes your config, so it is empty:", repr(r["source_language"]))
'Expected — 200 with an English translation. Note the quirk: with auto-detect, source_language echoes your (empty) configuration rather than the detected locale — use bidirectional mode (Step 4) if you need the detected locale reported.
Step 4 — Domain jargon and generational vernacular
Compose locale + jargon + vernacular to steer vocabulary and tone. Jargon values are PascalCase enum variants (Agriculture, Construction, Education, Engineering, Entertainment, Finance, Healthcare, InformationTechnologyAndTelecom, LayPerson, Legal, MarketingAndPr, Manufacturing, NewsAndJournalism, Property, Retail); vernacular values are BabyBoomers, GenerationX, Millennials, GenerationZ, GenerationAlpha, TheSilentGeneration, Neutral:
curl -s -X POST "${LIN_URL}/v3/translation/translate" \
-H "Content-Type: application/json" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Authorization: Bearer ${USER_TOKEN}" \
-d '{"source": {"locale": "en-US"}, "target": {"locale": "es-ES", "jargon": "Agriculture"}, "text": "The heifer is due to calve next week."}' \
| python3 -c '
import json, sys
r = json.load(sys.stdin)
assert "Agriculture" in r["target_language"], r["target_language"]
assert len(r["translated_text"]) > 0
print("jargon-steered:", r["target_language"], "->", r["translated_text"])
'Expected — 200; the configured jargon is echoed back inside target_language ("es-ES, Agriculture"), confirming the steer was applied. (The model is also instructed to add disclaimer suffixes when translating to/from legal, medical, or safety jargon.)
Step 5 — Bidirectional language pair (auto-flipping)
For bilingual UX (support chat, marketplace messaging), configure the pair once; Linga detects which language each input is in and translates to the other. language_a and language_b must each carry at least one field:
PAIR_A=$(curl -s -X POST "${LIN_URL}/v3/translation/translate-bidirectional" \
-H "Content-Type: application/json" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Authorization: Bearer ${USER_TOKEN}" \
-d '{"language_a": {"locale": "en-US"}, "language_b": {"locale": "es-ES"}, "text": "Good morning, how are the cows?"}')
echo "${PAIR_A}"
echo "${PAIR_A}" | python3 -c '
import json, sys
r = json.load(sys.stdin)
assert r["detected_source_locale"] == "en-US", r
assert "vaca" in r["translated_text"].lower() or "ganado" in r["translated_text"].lower(), r["translated_text"]
print("A->B ok")
'Expected response — 200 with original_text, translated_text, source_language / target_language (human-readable names this time, e.g. "English" / "Spanish"), and detected_source_locale: "en-US".
Now send Spanish down the same pair — the direction flips with no code change:
PAIR_B=$(curl -s -X POST "${LIN_URL}/v3/translation/translate-bidirectional" \
-H "Content-Type: application/json" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Authorization: Bearer ${USER_TOKEN}" \
-d '{"language_a": {"locale": "en-US"}, "language_b": {"locale": "es-ES"}, "text": "Buenos días, ¿cómo están las vacas?"}')
echo "${PAIR_B}"
echo "${PAIR_B}" | python3 -c '
import json, sys
r = json.load(sys.stdin)
assert r["detected_source_locale"] == "es-ES", r
assert "cow" in r["translated_text"].lower(), r["translated_text"]
print("B->A ok: direction flipped automatically")
'Expected — 200 with detected_source_locale: "es-ES" and an English translation. If the input matches neither configured language, the model is asked to reply LANGUAGE_NOT_IN_PAIR, which surfaces as 422 — a best-effort contract that depends on model compliance, so treat it as a graceful-degradation path rather than a guarantee.
Step 6 — Error contract
Missing target, missing key pair, and a short device_id on QA login each fail distinctly:
# 400 plain text — empty target
CODE=$(curl -s -o /tmp/g60-err1.txt -w '%{http_code}' -X POST "${LIN_URL}/v3/translation/translate" \
-H "Content-Type: application/json" \
-H "x-api-key: ${INAYA_API_KEY}" \
-H "x-client-app-key: ${INAYA_APP_KEY}" \
-H "Authorization: Bearer ${USER_TOKEN}" \
-d '{"source": {"locale": "en-US"}, "target": {}, "text": "hello"}')
[ "${CODE}" = "400" ] && grep -q "No target language specified" /tmp/g60-err1.txt \
&& echo "400 plain-text error ok: $(cat /tmp/g60-err1.txt)"
# 401 JSON — key pair omitted
CODE=$(curl -s -o /tmp/g60-err2.txt -w '%{http_code}' -X POST "${LIN_URL}/v3/translation/translate" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${USER_TOKEN}" \
-d '{"source": {}, "target": {"locale": "en-US"}, "text": "hola"}')
[ "${CODE}" = "401" ] && grep -q "Missing Tenant API Key" /tmp/g60-err2.txt \
&& echo "401 auth error ok: $(cat /tmp/g60-err2.txt)"Expected — 400 with a plain-text body (Invalid request: No target language specified) and 401 with a JSON body. Note the asymmetry: not all errors are Problem Details JSON — parse by status code first, then inspect the body format.
Putting it together
A single script that runs the whole journey (assumes USER_TOKEN from Step 1):
KEYS=(-H "x-api-key: $INAYA_API_KEY" -H "x-client-app-key: $INAYA_APP_KEY")
# 1. One-shot, explicit locales
curl -sf -X POST "$LIN_URL/v3/translation/translate" -H "Authorization: Bearer $USER_TOKEN" \
"${KEYS[@]}" -H "Content-Type: application/json" \
-d '{"source": {"locale": "en-US"}, "target": {"locale": "de-DE"}, "text": "The pump is running."}'
# 2. Auto-detected source
curl -sf -X POST "$LIN_URL/v3/translation/translate" -H "Authorization: Bearer $USER_TOKEN" \
"${KEYS[@]}" -H "Content-Type: application/json" \
-d '{"source": {}, "target": {"locale": "en-US"}, "text": "Guten Morgen."}'
# 3. Domain jargon
curl -sf -X POST "$LIN_URL/v3/translation/translate" -H "Authorization: Bearer $USER_TOKEN" \
"${KEYS[@]}" -H "Content-Type: application/json" \
-d '{"source": {"locale": "en-US"}, "target": {"locale": "es-ES", "jargon": "Agriculture"}, "text": "Rotate the pasture before overgrazing."}'
# 4. Bidirectional pair — two messages, two directions, same config
curl -sf -X POST "$LIN_URL/v3/translation/translate-bidirectional" -H "Authorization: Bearer $USER_TOKEN" \
"${KEYS[@]}" -H "Content-Type: application/json" \
-d '{"language_a": {"locale": "en-US"}, "language_b": {"locale": "es-ES"}, "text": "See you at the market."}'
curl -sf -X POST "$LIN_URL/v3/translation/translate-bidirectional" -H "Authorization: Bearer $USER_TOKEN" \
"${KEYS[@]}" -H "Content-Type: application/json" \
-d '{"language_a": {"locale": "en-US"}, "language_b": {"locale": "es-ES"}, "text": "Nos vemos en el mercado."}'(LIN_URL is the Linga base URL — http://127.0.0.1:40065 on the local stack; AUTH_URL is http://127.0.0.1:40004; TENANT_ID is your tenant.)
Try it live
Open the Playground → Translation & Voice Cloning — the Linga console exercises the same translate calls against the live service.
Copy agent prompt
You are working with the Inaya Labs Linga translation service at LIN_URL
(:40065, prefix /v3/translation), backed by Evergreen AI. The tenant key pair
is in INAYA_API_KEY / INAYA_APP_KEY, TENANT_ID holds the tenant, and AUTH_URL
(:40004) is the Auth Service. Every call needs BOTH the key-pair headers AND
a user JWT (obtain via POST AUTH /v3/auth/qa/login {"device_id" (min 8 chars),
"locale": "en-US", "tenant_id", "username"}; the JWT is forwarded to
Evergreen, so it must be valid). Steps: (1) Translate POST LIN
/v3/translation/translate {"source": {"locale": "en-US"}, "target": {"locale":
"es-ES"}, "text": "The chickens escaped again."} — assert 200 and snake_case
fields original_text/translated_text/source_language/target_language. (2)
Auto-detect: same endpoint with "source": {} and Spanish text — assert 200;
note source_language echoes the (empty) config. (3) Jargon: add
"jargon": "Agriculture" to the target and assert the response
target_language contains "Agriculture". (4) Bidirectional: POST
/v3/translation/translate-bidirectional {"language_a": {"locale": "en-US"},
"language_b": {"locale": "es-ES"}, "text": ...} first with English, then with
Spanish — assert detected_source_locale flips en-US -> es-ES and the
translations go the opposite ways. (5) Errors: empty target -> 400 PLAIN
TEXT "Invalid request: No target language specified"; omitting the key pair
-> 401 JSON. Note there is no batch endpoint and no language-list endpoint —
the jargon/vernacular enums are documented in the OpenAPI JSON at
/v3/translation/api-docs/openapi.json. Never log the JWT; report status codes
and translated_text per step.API reference
Next journeys
- Cloning a voice and speaking (G-61) — register a VoicePrint and generate speech on the Speech service.
- Your first AI chat session (G-10) — the Evergreen session mechanics Linga uses under the hood.
- Errors, retries & idempotency (G-04) — Linga's internal Evergreen retry window, and what a 500 means here.