# stapel-translate 0.5.7

AI-powered content translation: a TranslationEntry/TranslationValue store across a configurable language set, an LLM-routed translate.resolve comm Function with English-fallback, autofill/verification workflows, a staff dashboard (stats, per-language editing, LLM help, export/import, orphan cleanup), a Figma plugin API, and pluggable key collectors (backend error keys, notification templates, project-specific) so keys enter without any module importing another.

Contract: axes 2 · surface 19 · extension points 4.
Generated from docs/capabilities.json by `stapel-llms-txt` — do not edit; drift-gated by `make contract-check`.

## Configuration axes — what a product switches on
Settings keys; `default` is what you get by saying nothing. Turning an axis off unmounts the operations it gates.
- DEFAULT_LANGUAGE [enum, default "en"] — Source/fallback language
  Source/fallback language for translate.resolve and autofill (conf.py, MODULE.md Settings table).
- LLM_PROVIDER [enum, default "stapel_translate.providers.AgentProvider"] — LLM backend for translation
  Which backend performs a single-string translation for translate/verify/autofill. Default routes to stapel-agent by HTTP; CommAgentProvider routes through the llm.complete comm Function instead (in-process in a monolith); OpenAICompatibleProvider talks to any OpenAI-compatible endpoint directly (conf.py, MODULE.md Settings table).

## Usage surface — call these before writing your own
This is the answer to "does Stapel already have something for X?". `instead of` names the outside symbol this one displaces.
### permission_class
- IsAuthorizedTranslator — stapel_translate.permissions.IsAuthorizedTranslator
  instead of: rest_framework.permissions.IsAdminUser
  DRF permission gate for the translator/dashboard API surface: passes for staff/superusers or any email listed in AuthorizedTranslator with is_active=True. Put this on any endpoint that exposes translation content for editing instead of hand-rolling an is_staff check or a custom AuthorizedTranslator query — it is exactly the check the built-in dashboard and Figma-plugin views already share.
### gate_function
- agent_payload — stapel_translate.providers.agent_payload
  The llm.complete request body shared by AgentProvider and CommAgentProvider (model size + optional provider override). Reuse this when writing a third stapel-agent-backed provider instead of re-reading AGENT_MODEL_SIZE/AGENT_PROVIDER by hand.
- autofill_missing — stapel_translate.autofill.autofill_missing
  Fill missing (key, language) translation values via the configured LLM provider, writing verified=False and a TranslationHistory row per fill. This is the core logic behind the translate.autofill comm task and the autofill_translations command — call it directly instead of looping get_llm_provider().translate() yourself when triggering autofill from custom code (e.g. right after a new language is added).
- autofill_targets — stapel_translate.autofill.autofill_targets
  The configured languages eligible for autofill (every supported language except the source/default one, optionally narrowed to a caller-given subset). Use this instead of filtering get_supported_languages() yourself so a custom autofill trigger agrees with the built-in task/command on which languages are ever auto-translated.
- collect_error_keys_from_services — stapel_translate.error_collector.collect_error_keys_from_services
  Fan out to every registered service's error-keys endpoint (with per-service mount-point discovery) and upsert TranslationEntry rows tagged source='backend:errors', clearing stale refs first. This is what the 'errors' collector and the dashboard's key-collection button run — call it directly instead of writing your own peer-discovery loop over stapel_core.django.nav.get_services().
- collect_notification_keys — stapel_translate.notification_collector.collect_notification_keys
  Fetch the notifications service's notification-keys endpoint (with mount-point discovery, cached per process) and upsert TranslationEntry rows tagged source='backend:notifications'. This is what the 'notifications' collector runs — call it directly instead of hand-rolling the same peer-discovery + upsert logic; NOTIFICATION_KEYS_PATHS already carries the pre-/post-v1-canon mount-point history so a resolver-404 fails loudly (PeerRouteUnavailable) rather than silently reporting zero keys.
- extract_agent_result — stapel_translate.providers.extract_agent_result
  Parse the {status, result} envelope stapel-agent returns (HTTP or comm alike) into cleaned text, raising TranslationProviderError on a non-ok status or an empty/malformed result. Explicitly the shared HTTP/comm parsing step (per its own docstring) — reuse it in any custom provider that talks to stapel-agent through a different transport instead of re-deriving the envelope shape.
- get_agent_url — stapel_translate.providers.get_agent_url
  Base URL of the stapel-agent service (AGENT_SERVICE_URL, trailing slash stripped). Use this instead of reading translate_settings.AGENT_SERVICE_URL directly when writing a custom LLM_PROVIDER that still talks to stapel-agent, so the normalization stays in one place.
- get_default_language — stapel_translate.conf.get_default_language
  The source/fallback language code (DEFAULT_LANGUAGE) — call this instead of hardcoding 'en' or reading translate_settings.DEFAULT_LANGUAGE directly; translate.resolve, autofill and the dashboard all derive their fallback from this single call.
- get_language_names — stapel_translate.conf.get_language_names
  {code: display name} for every configured language, with DEFAULT_LANGUAGE_NAMES filling in any configured code that has no override. Use this instead of reading translate_settings.LANGUAGE_NAMES directly when rendering a language picker, so an unnamed configured language still gets a sensible label instead of a KeyError or a blank.
- get_supported_languages — stapel_translate.conf.get_supported_languages
  instead of: django.conf.global_settings.LANGUAGES
  The configured language codes. Call this instead of reading translate_settings.LANGUAGES (or django.conf.settings.LANGUAGES) directly: it detects and discards Django's own untouched global LANGUAGES default — which AppSettings' flat-setting fallback would otherwise pick up as if it were a deliberate override — and unwraps Django-style (code, name) tuples.
- get_translator_info — stapel_translate.permissions.get_translator_info
  instead of: AuthorizedTranslator.objects.get()
  Look up the AuthorizedTranslator record for a user by email (None if unauthenticated or not registered). Call this instead of AuthorizedTranslator.objects.get(email=...) directly — it already absorbs the anonymous-user and DoesNotExist cases a naive lookup would need to special-case.
- get_translator_name — stapel_translate.permissions.get_translator_name
  Display name for translation-history logging: the AuthorizedTranslator's name, else the user's full name/email, else empty. Use this instead of user.get_full_name() directly so history entries stay attributable even for a translator record with no linked platform account.
- get_user_allowed_languages — stapel_translate.permissions.get_user_allowed_languages
  The language whitelist a user may edit/verify — None means unrestricted (a privileged user, or a translator whose allowed_languages is empty). Use this rather than reading AuthorizedTranslator.allowed_languages directly: it folds in the is_privileged_user short-circuit and the 'empty list = all languages' convention.
### predicate
- can_edit_language — stapel_translate.permissions.can_edit_language
  Whether a user may edit/verify a given language — the one call the dashboard/Figma views make before accepting a translation write. Reach for this instead of re-deriving it from get_user_allowed_languages() in a new endpoint; a per-view reimplementation is exactly how a translator-scoping gap would slip past one endpoint and not the others.
- is_privileged_user — stapel_translate.permissions.is_privileged_user
  Whether a user is staff or superuser — the shortcut every other check here (get_user_allowed_languages, etc.) short-circuits on before consulting AuthorizedTranslator. Reach for this instead of writing user.is_superuser or user.is_staff inline so the privilege definition stays in one place.
### factory
- get_collectors — stapel_translate.collectors.get_collectors
  {name: callable} for every registered collector, dotted-path entries resolved on access. Call this instead of reaching into the module's internal registry dict directly when building a custom key-collection command or UI.
- get_llm_provider — stapel_translate.providers.get_llm_provider
  Instantiate the configured LLM_PROVIDER. Call this instead of importing a specific provider class directly whenever custom code (a management command, a one-off script) needs to run a translation exactly the way autofill/LLM-help do.
- register_collector — stapel_translate.collectors.register_collector
  Register a translation-key collector (a zero-arg callable returning a stats dict) under a name — the fork-free way to make collect_translations and the dashboard's 'Collect keys' button pick up a project-specific key source, alongside the built-in errors/notifications collectors.

## Extension points — what a product replaces, fork-free
- LLM_PROVIDER [dotted_path]
  Resolved via import_string in get_llm_provider(); contract: translate(key, english_text, target_language, context) -> str; subclass providers.BaseTranslationProvider to reuse prompt building (conf.py, MODULE.md 'Settings').
- collectors [named_registry]
  register_collector('name', callable_or_dotted_path) (collectors.py): a zero-arg callable returning a stats dict, run by collect_translations and the dashboard 'Collect keys' button — the project-specific-keys channel (MODULE.md 'Adding / overriding translations').
- serializer_seams [class_override]
  mixins.SerializerSeamMixin: views declare request_serializer_class/response_serializer_class (or purpose-prefixed variants) instantiated only through matching get_*_serializer_class() getters (MODULE.md 'Serializer seams').
- translate.resolve [comm_function]
  Provided Function: {keys, language} -> {values}, DEFAULT_LANGUAGE fallback, missing keys omitted (never null), soft-deleted entries never resolved (functions.py, MODULE.md 'Functions').

## Fits with — fleet dependencies
- stapel-agent (optional) — default LLM_PROVIDER (AgentProvider) calls stapel-agent over HTTP (AGENT_SERVICE_URL) or via the llm.complete comm Function (CommAgentProvider); no direct Python import either way (conf.py AGENT_SERVICE_URL, MODULE.md Settings table)
- stapel-core (required) — comm bus (translate.resolve Function, translations.changed emit, task_handler), RevisionMixin, access categories, error registry (pyproject.toml dependency)
