boffire commited on
Commit
63c4f33
·
verified ·
1 Parent(s): 14d9583

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +18 -32
app.py CHANGED
@@ -103,20 +103,21 @@ def translate_libre_all_variants(text):
103
  return results
104
 
105
 
106
- # --- HTML TEMPLATES (abbreviated for brevity) ---
107
- HTML_TEMPLATE = """<!DOCTYPE html><html lang="en"><head>...</head><body>...</body></html>"""
108
- API_DOCS_TEMPLATE = """<!DOCTYPE html><html lang="en"><head>...</head><body>...</body></html>"""
 
109
 
110
  app = Flask(__name__)
111
  CORS(app, resources={r"/*": {"origins": "*"}})
112
 
113
- # Global error handler
114
  @app.errorhandler(Exception)
115
  def handle_exception(e):
116
  print(f"[GLOBAL ERROR] {type(e).__name__}: {e}", flush=True)
117
  import traceback
118
  traceback.print_exc()
119
- return jsonify({"error": str(e)}), 500
120
 
121
  # Pre-load model
122
  try:
@@ -141,10 +142,6 @@ def index():
141
  return render_template_string(HTML_TEMPLATE, marian=marian, libre=libre, source_text=source_text)
142
 
143
 
144
- # =============================================================================
145
- # LIBRETRANSLATE-COMPATIBLE ENDPOINTS
146
- # =============================================================================
147
-
148
  def _extract_text(data):
149
  """Extract text from q field. Weblate sends q as either string or list."""
150
  q = data.get("q", "")
@@ -153,18 +150,17 @@ def _extract_text(data):
153
  return str(q).strip()
154
 
155
 
156
- @app.route("/translate", methods=["GET", "POST"])
157
  def libretranslate_translate():
158
  """
159
  LibreTranslate-compatible translation endpoint.
160
- Accepts GET (for validation) and POST (for actual translation).
161
  """
162
  print(f"[WEblate] === /translate called ({request.method}) ===", flush=True)
163
- print(f"[WEblate] Content-Type: {request.content_type}", flush=True)
164
 
 
165
  if request.method == "GET":
166
- # Some clients might do GET for validation
167
- return jsonify({"message": "POST required for translation"}), 200
168
 
169
  try:
170
  raw_data = request.get_data(as_text=True)
@@ -211,8 +207,8 @@ def libretranslate_translate():
211
  print(f"[WEblate] text={text[:50]}, source={source}, target={target}", flush=True)
212
 
213
  if not text:
214
- print(f"[WEblate] ERROR: empty text", flush=True)
215
- return jsonify({"error": "No text provided"}), 400
216
 
217
  # Handle auto source
218
  if source == "auto":
@@ -249,13 +245,14 @@ def libretranslate_translate():
249
 
250
  response = {"translatedText": best_translation or ""}
251
  print(f"[WEblate] SUCCESS: {response}", flush=True)
252
- return jsonify(response)
253
 
254
  except Exception as e:
255
  print(f"[WEblate] Translation failed: {e}", flush=True)
256
  import traceback
257
  traceback.print_exc()
258
- return jsonify({"translatedText": "", "error": str(e)}), 500
 
259
 
260
 
261
  @app.route("/languages", methods=["GET"])
@@ -267,7 +264,7 @@ def libretranslate_languages():
267
  ])
268
  except Exception as e:
269
  print(f"[WEblate] /languages error: {e}", flush=True)
270
- return jsonify([]), 500
271
 
272
 
273
  @app.route("/detect", methods=["POST"])
@@ -282,35 +279,24 @@ def libretranslate_detect():
282
  return jsonify([{"confidence": 0.99, "language": detected}])
283
  except Exception as e:
284
  print(f"[WEblate] /detect error: {e}", flush=True)
285
- return jsonify([]), 500
286
 
287
 
288
  @app.route("/frontend/settings", methods=["GET"])
289
  def libretranslate_settings():
290
- """
291
- LibreTranslate frontend settings endpoint.
292
- Some clients check this for API capabilities.
293
- """
294
  return jsonify({
295
  "apiKeys": False,
296
  "charLimit": 5000,
297
  "dailyLimit": -1,
298
  "frontendTimeout": 5000,
299
  "keyRequired": False,
300
- "language": {
301
- "source": {"code": "en", "name": "English"},
302
- "target": {"code": "kab", "name": "Kabyle"}
303
- },
304
  "suggestions": False,
305
  "supportedFilesFormat": ["text", "html"],
306
  "theme": "auto"
307
  })
308
 
309
 
310
- # =============================================================================
311
- # NATIVE API
312
- # =============================================================================
313
-
314
  @app.route("/api/docs", methods=["GET"])
315
  def api_docs():
316
  base_url = request.url_root.rstrip("/")
 
103
  return results
104
 
105
 
106
+ # --- HTML TEMPLATES ---
107
+ HTML_TEMPLATE = """<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Kabyle Translation Hub</title><style>:root{--bg-primary:#0f172a;--bg-secondary:#1e293b;--bg-tertiary:#334155;--accent-primary:#6366f1;--accent-secondary:#8b5cf6;--accent-success:#10b981;--text-primary:#f1f5f9;--text-secondary:#94a3b8;--border:#475569;--radius:12px;--radius-sm:8px}*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;background:linear-gradient(135deg,var(--bg-primary) 0%,#1a1a2e 100%);color:var(--text-primary);min-height:100vh;padding:20px}.container{max-width:1000px;margin:0 auto}header{text-align:center;padding:40px 20px}header h1{font-size:2.5rem;font-weight:800;background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:10px}header p{color:var(--text-secondary);font-size:1.1rem}.input-card{background:var(--bg-secondary);border-radius:var(--radius);padding:24px;margin-bottom:24px;border:1px solid var(--border)}.input-group{display:flex;flex-direction:column;gap:16px}textarea{width:100%;padding:16px;background:var(--bg-tertiary);border:2px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);font-size:16px;resize:vertical;min-height:120px;font-family:inherit;transition:border-color .2s}textarea:focus{outline:none;border-color:var(--accent-primary)}textarea::placeholder{color:var(--text-secondary)}.btn-primary{background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));color:white;border:none;padding:16px 32px;border-radius:var(--radius-sm);font-size:16px;font-weight:600;cursor:pointer;transition:transform .2s,box-shadow .2s;display:flex;align-items:center;justify-content:center;gap:8px}.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 30px rgba(99,102,241,.3)}.btn-primary:disabled{opacity:.6;cursor:not-allowed;transform:none}.results-container{display:flex;flex-direction:column;gap:20px}.engine-card{background:var(--bg-secondary);border-radius:var(--radius);border:1px solid var(--border);overflow:hidden}.engine-header{padding:20px 24px;background:var(--bg-tertiary);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}.engine-title{display:flex;align-items:center;gap:12px;font-size:1.1rem;font-weight:600}.engine-badge{font-size:.75rem;padding:4px 12px;background:var(--bg-secondary);border-radius:20px;color:var(--text-secondary);border:1px solid var(--border)}.translation-list{list-style:none}.translation-item{padding:20px 24px;border-bottom:1px solid var(--border);cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:space-between;gap:16px}.translation-item:last-child{border-bottom:none}.translation-item:hover{background:rgba(99,102,241,.1)}.translation-item.selected{background:rgba(16,185,129,.15);border-left:4px solid var(--accent-success)}.translation-text{flex:1;font-size:1.05rem;line-height:1.6;color:var(--text-primary)}.copy-icon{opacity:0;color:var(--accent-primary);transition:opacity .2s;flex-shrink:0}.translation-item:hover .copy-icon{opacity:1}.variant-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1px;background:var(--border)}.variant-item{background:var(--bg-secondary);padding:16px 20px;cursor:pointer;transition:all .2s;display:flex;flex-direction:column;gap:8px;position:relative}.variant-item:hover{background:rgba(99,102,241,.1)}.variant-item.selected{background:rgba(16,185,129,.15);box-shadow:inset 4px 0 0 var(--accent-success)}.variant-header{display:flex;align-items:center;justify-content:space-between}.variant-name{font-size:.75rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;font-weight:600}.variant-code{font-size:.7rem;color:var(--text-secondary);font-family:monospace;background:var(--bg-tertiary);padding:2px 8px;border-radius:4px}.variant-text{font-size:1rem;color:var(--text-primary);line-height:1.5}.variant-item.error{opacity:.6;cursor:default}.variant-item.error .variant-text{color:#ef4444;font-family:monospace;font-size:.85rem}.source-display{background:var(--bg-tertiary);padding:20px 24px;border-radius:var(--radius);margin-bottom:20px;border:1px solid var(--border)}.source-label{font-size:.75rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px}.source-text{font-size:1.1rem;color:var(--text-primary);line-height:1.6}.check-icon{color:var(--accent-success);opacity:0;transition:opacity .2s}.translation-item.selected .check-icon,.variant-item.selected .check-icon{opacity:1}.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(100px);background:var(--accent-success);color:white;padding:12px 24px;border-radius:100px;font-weight:600;opacity:0;transition:all .3s ease;z-index:1000;box-shadow:0 10px 30px rgba(0,0,0,.3)}.toast.show{transform:translateX(-50%) translateY(0);opacity:1}.toast.error{background:#ef4444}.loading{display:inline-block;width:18px;height:18px;border:2px solid rgba(255,255,255,.3);border-radius:50%;border-top-color:white;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:60px 20px;color:var(--text-secondary)}.empty-state svg{width:64px;height:64px;margin-bottom:16px;opacity:.5}.api-note{text-align:center;margin-top:32px;color:var(--text-secondary);font-size:.85rem}.api-note code{background:var(--bg-tertiary);padding:2px 8px;border-radius:4px;font-family:monospace}.api-note a{color:var(--accent-primary);text-decoration:none}.api-note a:hover{text-decoration:underline}@media(max-width:640px){header h1{font-size:1.75rem}.variant-grid{grid-template-columns:1fr}}</style></head><body><div class="container"><header><h1>English → Kabyle</h1><p>Choose the translation that suits you best</p></header><div class="input-card"><form method="POST" id="translateForm"><div class="input-group"><textarea name="text" id="inputText" placeholder="Enter English text to translate..." required autocomplete="off">{{ request.form.get('text', '') }}</textarea><button type="submit" class="btn-primary" id="submitBtn"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg><span>Translate</span></button></div></form></div>{% if source_text %}<div class="source-display"><div class="source-label">Source Text</div><div class="source-text">{{ source_text }}</div></div><div class="results-container"><div class="engine-card"><div class="engine-header"><div class="engine-title"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent-primary)"><rect x="4" y="4" width="16" height="16" rx="2"></rect><rect x="9" y="9" width="6" height="6"></rect></svg>MarianMT (Neural)</div><span class="engine-badge">{{ marian|length }} options</span></div><ul class="translation-list">{% for trans in marian %}<li class="translation-item" data-text="{{ trans }}" data-kind="translation"><span class="translation-text">{{ trans }}</span><svg class="check-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"></polyline></svg><svg class="copy-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></li>{% endfor %}</ul></div><div class="engine-card"><div class="engine-header"><div class="engine-title"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent-success)"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>LibreTranslate Variants</div><span class="engine-badge">{{ libre|length }} variants</span></div><div class="variant-grid">{% for variant_name, variant_data in libre.items() %}{% if variant_data.success %}<div class="variant-item" data-text="{{ variant_data.text }}" data-kind="variant"><div class="variant-header"><span class="variant-name">{{ variant_name }}</span><span class="variant-code">{{ variant_data.code }}</span></div><div class="variant-text">{{ variant_data.text }}</div><svg class="check-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" style="position:absolute;top:16px;right:16px"><polyline points="20 6 9 17 4 12"></polyline></svg></div>{% else %}<div class="variant-item error"><div class="variant-header"><span class="variant-name">{{ variant_name }}</span><span class="variant-code">{{ variant_data.code }}</span></div><div class="variant-text">{{ variant_data.text }}</div></div>{% endif %}{% endfor %}</div></div></div>{% else %}<div class="empty-state"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg><p>Enter text above and click Translate to see results</p></div>{% endif %}<div class="api-note">API available at <code>POST /api/translate</code> — see <a href="/api/docs">full API documentation</a>.<br>LibreTranslate-compatible endpoints: <code>/translate</code>, <code>/languages</code>, <code>/detect</code></div></div><div class="toast" id="toast">Copied to clipboard!</div><script>async function copyText(text){let copied=false;if(navigator.clipboard&&window.isSecureContext){try{await navigator.clipboard.writeText(text);copied=true}catch(err){copied=false}}if(!copied){try{const textArea=document.createElement('textarea');textArea.value=text;textArea.style.position='fixed';textArea.style.left='-9999px';document.body.appendChild(textArea);textArea.focus();textArea.select();copied=document.execCommand('copy');document.body.removeChild(textArea)}catch(err){copied=false}}return copied}function showToastMessage(message,isError=false){const toast=document.getElementById('toast');toast.textContent=message;toast.classList.toggle('error',isError);toast.classList.add('show');setTimeout(()=>toast.classList.remove('show'),2000)}function setupClickToCopy(){document.querySelectorAll('.translation-item[data-text]').forEach(item=>{item.addEventListener('click',async function(){document.querySelectorAll('.translation-item').forEach(el=>el.classList.remove('selected'));this.classList.add('selected');const copied=await copyText(this.dataset.text);showToastMessage(copied?'Copied to clipboard!':'Copy failed — please copy manually',!copied)})});document.querySelectorAll('.variant-item[data-text]').forEach(item=>{item.addEventListener('click',async function(){document.querySelectorAll('.variant-item').forEach(el=>el.classList.remove('selected'));this.classList.add('selected');const copied=await copyText(this.dataset.text);showToastMessage(copied?'Copied to clipboard!':'Copy failed — please copy manually',!copied)})})}document.addEventListener('DOMContentLoaded',setupClickToCopy);const translateForm=document.getElementById('translateForm');if(translateForm){translateForm.addEventListener('submit',function(){const btn=document.getElementById('submitBtn');btn.disabled=true;btn.innerHTML='<span class="loading"></span> Translating...'})}</script></body></html>"""
108
+
109
+ API_DOCS_TEMPLATE = """<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Kabyle Translation Hub - API Docs</title><style>:root{--bg-primary:#0f172a;--bg-secondary:#1e293b;--bg-tertiary:#334155;--accent-primary:#6366f1;--accent-secondary:#8b5cf6;--accent-success:#10b981;--text-primary:#f1f5f9;--text-secondary:#94a3b8;--border:#475569}*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;background:linear-gradient(135deg,var(--bg-primary) 0%,#1a1a2e 100%);color:var(--text-primary);min-height:100vh;padding:20px}.container{max-width:860px;margin:0 auto;padding-bottom:60px}header{padding:40px 0 20px}header h1{font-size:2.2rem;font-weight:800;background:linear-gradient(135deg,var(--accent-primary),var(--accent-secondary));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px}header p{color:var(--text-secondary)}header a{color:var(--accent-primary);text-decoration:none}header a:hover{text-decoration:underline}.section{background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;padding:24px;margin-bottom:20px}.section h2{font-size:1.2rem;margin-bottom:12px;color:var(--text-primary)}.section h2 .method{display:inline-block;font-size:.75rem;font-weight:700;padding:3px 10px;border-radius:6px;margin-right:10px;font-family:monospace}.method.get{background:rgba(16,185,129,.2);color:var(--accent-success)}.method.post{background:rgba(99,102,241,.2);color:var(--accent-primary)}p{color:var(--text-secondary);margin-bottom:12px;line-height:1.6}code.inline{background:var(--bg-tertiary);padding:2px 8px;border-radius:4px;font-family:monospace;color:var(--text-primary);font-size:.9em}pre{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;padding:16px;overflow-x:auto;font-family:monospace;font-size:.85rem;line-height:1.6;color:#e2e8f0;margin-bottom:12px}table{width:100%;border-collapse:collapse;margin-bottom:12px}th,td{text-align:left;padding:8px 12px;border-bottom:1px solid var(--border);font-size:.9rem}th{color:var(--text-secondary);font-weight:600}td{color:var(--text-primary)}td code{font-family:monospace;color:var(--accent-primary)}.back-link{display:inline-block;margin-bottom:20px}</style></head><body><div class="container"><a class="back-link" href="/">&larr; Back to translator</a><header><h1>Kabyle Translation Hub — API Docs</h1><p>Base URL: <code class="inline">{{ base_url }}</code></p></header><div class="section"><h2><span class="method post">POST</span>/api/translate</h2><p>Native API for translation with multiple engines and variants.</p><table><tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr><tr><td><code>text</code></td><td>string</td><td>yes</td><td>English text to translate</td></tr><tr><td><code>engines</code></td><td>array</td><td>no</td><td>Subset of <code>["marian","libre"]</code>. Defaults to both.</td></tr><tr><td><code>variants</code></td><td>array</td><td>no</td><td>Subset of LibreTranslate variant codes. Defaults to all.</td></tr></table><pre>curl -X POST "{{ base_url }}/api/translate" \\\n -H "Content-Type: application/json" \\\n -d '{"text": "Good morning", "engines": ["marian", "libre"]}'</pre></div><div class="section"><h2><span class="method post">POST</span>/translate</h2><p><strong>LibreTranslate-compatible endpoint</strong> for Weblate and other tools.</p><table><tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr><tr><td><code>q</code></td><td>string or array</td><td>yes</td><td>Text to translate</td></tr><tr><td><code>source</code></td><td>string</td><td>yes</td><td>Source language code</td></tr><tr><td><code>target</code></td><td>string</td><td>yes</td><td>Target language code</td></tr><tr><td><code>api_key</code></td><td>string</td><td>no</td><td>Ignored</td></tr></table><pre>curl -X POST "{{ base_url }}/translate" \\\n -H "Content-Type: application/json" \\\n -d '{"q": "Good morning", "source": "en", "target": "kab"}'</pre><p><strong>Weblate config:</strong></p><pre>MT_LIBRETRANSLATE_API_URL = "{{ base_url }}"</pre></div><div class="section"><h2><span class="method get">GET</span>/languages</h2><p>Supported language pairs.</p><pre>curl "{{ base_url }}/languages"</pre></div><div class="section"><h2><span class="method post">POST</span>/detect</h2><p>Language detection stub.</p><pre>curl -X POST "{{ base_url }}/detect" -d "q=Hello world"</pre></div><div class="section"><h2><span class="method get">GET</span>/health</h2><p>Health check.</p><pre>curl "{{ base_url }}/health"</pre></div></div></body></html>"""
110
 
111
  app = Flask(__name__)
112
  CORS(app, resources={r"/*": {"origins": "*"}})
113
 
114
+ # Global error handler - NEVER return 500 to clients
115
  @app.errorhandler(Exception)
116
  def handle_exception(e):
117
  print(f"[GLOBAL ERROR] {type(e).__name__}: {e}", flush=True)
118
  import traceback
119
  traceback.print_exc()
120
+ return jsonify({"translatedText": "", "error": str(e)}), 200
121
 
122
  # Pre-load model
123
  try:
 
142
  return render_template_string(HTML_TEMPLATE, marian=marian, libre=libre, source_text=source_text)
143
 
144
 
 
 
 
 
145
  def _extract_text(data):
146
  """Extract text from q field. Weblate sends q as either string or list."""
147
  q = data.get("q", "")
 
150
  return str(q).strip()
151
 
152
 
153
+ @app.route("/translate", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
154
  def libretranslate_translate():
155
  """
156
  LibreTranslate-compatible translation endpoint.
157
+ NEVER returns 500 - always returns 200 with either translation or empty string.
158
  """
159
  print(f"[WEblate] === /translate called ({request.method}) ===", flush=True)
 
160
 
161
+ # GET requests - just return empty translation
162
  if request.method == "GET":
163
+ return jsonify({"translatedText": ""}), 200
 
164
 
165
  try:
166
  raw_data = request.get_data(as_text=True)
 
207
  print(f"[WEblate] text={text[:50]}, source={source}, target={target}", flush=True)
208
 
209
  if not text:
210
+ print(f"[WEblate] Empty text, returning empty translation", flush=True)
211
+ return jsonify({"translatedText": ""}), 200
212
 
213
  # Handle auto source
214
  if source == "auto":
 
245
 
246
  response = {"translatedText": best_translation or ""}
247
  print(f"[WEblate] SUCCESS: {response}", flush=True)
248
+ return jsonify(response), 200
249
 
250
  except Exception as e:
251
  print(f"[WEblate] Translation failed: {e}", flush=True)
252
  import traceback
253
  traceback.print_exc()
254
+ # NEVER return 500
255
+ return jsonify({"translatedText": ""}), 200
256
 
257
 
258
  @app.route("/languages", methods=["GET"])
 
264
  ])
265
  except Exception as e:
266
  print(f"[WEblate] /languages error: {e}", flush=True)
267
+ return jsonify([]), 200
268
 
269
 
270
  @app.route("/detect", methods=["POST"])
 
279
  return jsonify([{"confidence": 0.99, "language": detected}])
280
  except Exception as e:
281
  print(f"[WEblate] /detect error: {e}", flush=True)
282
+ return jsonify([]), 200
283
 
284
 
285
  @app.route("/frontend/settings", methods=["GET"])
286
  def libretranslate_settings():
 
 
 
 
287
  return jsonify({
288
  "apiKeys": False,
289
  "charLimit": 5000,
290
  "dailyLimit": -1,
291
  "frontendTimeout": 5000,
292
  "keyRequired": False,
293
+ "language": {"source": {"code": "en", "name": "English"}, "target": {"code": "kab", "name": "Kabyle"}},
 
 
 
294
  "suggestions": False,
295
  "supportedFilesFormat": ["text", "html"],
296
  "theme": "auto"
297
  })
298
 
299
 
 
 
 
 
300
  @app.route("/api/docs", methods=["GET"])
301
  def api_docs():
302
  base_url = request.url_root.rstrip("/")