# Sentiary documentation > Get started with Sentiary 2.0: CLI, MCP, configuration, Gradle, and API guides Sentiary manages localization terms and translations. Use the CLI to download translations, configure projects and outputs in TOML, or integrate generated resources into Gradle builds. The sections below cover getting started, the complete CLI, configuration and offline use, Gradle integration, API authentication, and migration and troubleshooting. For the complete REST API schema, use [OpenAPI](api/openapi.yaml). # Get started # Sentiary documentation Manage translations in Sentiary and deliver them to your application with CLI and Gradle 2.0. - **Use the CLI** Install the CLI, create `sentiary.toml`, and download your first translations. [Get started]() - **Connect Gradle** Generate and stage Android and Compose resources as part of your build. [Set up Gradle]() - **Connect an AI assistant** Manage projects, languages, and translations through hosted MCP. [Set up MCP]() - **Explore the API** Find authentication requirements, endpoints, and request and response schemas. [Open the API guide]() ``` sentiary-cli init sentiary-cli pull --dry-run sentiary-cli pull ``` Start with an existing Sentiary project and its language IDs. Follow the [first download guide]() for credentials and generated-file ownership. [Contribute]() · [Browse by topic]() · [Migrate from Gradle 1.x]() · [Troubleshooting]() For AI tools: [llms.txt]() · [Full documentation]() # Installation ## Install Download the installer for inspection, then run it: ``` curl -fsSL https://raw.githubusercontent.com/MateeDevs/sentiary-tools/main/go/install.sh -o install-sentiary.sh sh install-sentiary.sh ``` It needs `curl` or `wget`, `tar`, and `sha256sum` or `shasum`. It detects OS and architecture, downloads the archive and `SHA256SUMS`, and checks the archive before extraction and installation. The default directory is `/usr/local/bin` when writable, otherwise `$HOME/.local/bin`. Override it with `INSTALL_DIR`. The installer prints a PATH reminder; it does not edit shell startup files. ``` INSTALL_DIR="$HOME/.local/bin" sh install-sentiary.sh export PATH="$HOME/.local/bin:$PATH" sentiary-cli --version ``` Add the PATH export to your shell startup file if needed. To pin a release, replace `vX.Y.Z` with an existing release tag: ``` SENTIARY_VERSION=vX.Y.Z sh install-sentiary.sh # Alternatively, use the first argument: sh install-sentiary.sh vX.Y.Z ``` `SENTIARY_VERSION` takes precedence over the first argument. With neither, the installer uses GitHub's latest release. `INSTALL_DIR` and the version controls are installer inputs, not CLI flags. ``` Invoke-WebRequest https://raw.githubusercontent.com/MateeDevs/sentiary-tools/main/go/install.ps1 -OutFile install-sentiary.ps1 .\install-sentiary.ps1 sentiary-cli --version ``` PowerShell execution policy must permit running the downloaded script. The default directory is `$env:LOCALAPPDATA\Programs\Sentiary\bin`; `INSTALL_DIR` overrides it. The installer adds that directory to both the user PATH and the current process PATH. It checks the downloaded executable using `Get-FileHash -Algorithm SHA256` before replacing the installed binary. ``` $env:INSTALL_DIR = "$env:LOCALAPPDATA\Programs\Sentiary\bin" .\install-sentiary.ps1 -Version vX.Y.Z ``` `-Version` defaults to `SENTIARY_VERSION`; an explicit parameter wins. Without either, it uses the latest release. Both AMD64 and ARM64 executables are available. ## Platforms and releases The release workflow builds AMD64 and ARM64 for macOS (`darwin`), Linux (`linux`), and Windows (`windows`). Intel/x86-64 maps to `amd64`; Apple Silicon/aarch64 maps to `arm64`. The installers reject other architectures. There is no 32-bit installer target. Download assets from [Sentiary Tools releases](). Unix assets are `sentiary-cli--.tar.gz`; Windows assets are `sentiary-cli-windows-.exe`. `SHA256SUMS` accompanies releases. ## Manual checksum verification Download the asset and `SHA256SUMS` from the **same release**. On Linux, use `sha256sum `; on macOS use `shasum -a 256 `; on Windows use `Get-FileHash -Algorithm SHA256`. Compare the complete hash with the exact asset's line in `SHA256SUMS`. Reject missing, ambiguous, or mismatched entries. For Unix, verify the archive before extracting it. The manifest is fetched from the release; it is not an independently signed attestation. ## Upgrade and inspect Rerun the installer to replace the binary with the latest release, or specify an existing tag for a reproducible upgrade/downgrade. It does not migrate your TOML, generated files, or cache. Check which binary your shell resolves (`command -v sentiary-cli` or `Get-Command sentiary-cli`) if the reported version is unexpected. `version`, `--version`, and `-v` print the version. `version --json` prints the version and supported integration protocols. See [protocol negotiation](). For local development, see [building the CLI from source](). # First configured download Start with an existing Sentiary project ID and its language IDs. The current `init` wizard accepts those IDs locally; it does not browse or create remote projects. ``` export SENTIARY_USER_API_KEY='YOUR_USER_API_KEY' sentiary-cli init --directory . ``` The directory must already exist. The wizard asks for project aliases, IDs, language selections, output names, formats, paths, and locale mappings. It supports multiple projects and outputs. For combined Xcode catalogs it asks for a selected source language. Enter `cancel` at a prompt or end stdin to abort. It refuses to overwrite `sentiary.toml` or a local credentials file that it would create. The defaults produce Android/Compose paths under `build/sentiary/`, or a combined `Localizable.xcstrings`. Generic formats require an explicit path. The preview redacts project keys and requires a final `y` before writing. Saving a project key from `SENTIARY_PROJECT_API_KEY` in shared TOML or a user key from `SENTIARY_USER_API_KEY` in local TOML requires explicit opt-in. The default uses environment credentials. New config files use mode `0600` where supported. Ignore entries for `sentiary.local.toml` and `.sentiary/` are offered by default. Alternatively, copy a [mobile configuration](), replace project IDs, and edit languages and paths. Do not commit personal credentials. The local supplement supports only `user_api_key`. ``` sentiary-cli pull --dry-run sentiary-cli pull sentiary-cli pull --output android ``` The preview prints resolved file paths as JSON. Wildcard languages and aliases require a read request to resolve project languages. It does not download exports or write generated files. An ordinary online preview still requires credentials. A pull downloads and verifies the full selection, publishes a completed cache set, then replaces generated files. Existing files need deliberate [adoption](). Review the plan and affected files before using `--force`. ``` sentiary-cli pull --offline ``` Offline mode restores the newest single completed set covering the request. It needs no credentials or network, but needs a previous successful online pull and intact cache objects. See [offline recovery](). # CLI # CLI reference ``` sentiary-cli init sentiary-cli pull --dry-run sentiary-cli pull ``` Use [mobile downloads]() for Android, Compose, and Xcode examples. The reference below covers every command and option. ## Invocation, credentials, and streams Use `sentiary-cli COMMAND [OPTIONS]`. All options below belong after the command. There are no global credential, server, or JSON switches. Go flag syntax accepts `--flag=value` and `--flag value` (also single-dash forms). Boolean flags accept `--flag` for true or `--flag=false`; do not use a separate boolean word. Positional arguments are rejected. Scalar options may be repeated, with the last value winning; only `pull --output` accumulates multiple values. Comma-separated language lists are one scalar option. `SENTIARY_BASE_URL` optionally overrides the default `https://api.sentiary.com` for API commands and online pulls; it also scopes the export cache. All API commands require `SENTIARY_USER_API_KEY`; project commands use `--project-id` then `SENTIARY_PROJECT_ID`. They do not load TOML or project keys. Authorization and project-role requirements are enforced by the API: a user key does not grant access beyond its owner's permissions. `pull` instead uses [TOML credential precedence](). `init`, help, and version need no network credentials; offline pull needs none. Management and string commands write indented JSON to stdout, including mutations. `export` writes raw text to stdout or its output file; `import` reads a file or all of stdin and writes a JSON import result. `pull` normally prints `Downloaded N file(s).`, `--dry-run` prints a JSON array, and `--json` prints a protocol result. `init` reads line-oriented answers from stdin, sends prompts/redacted preview to stderr, and prints completion to stdout. Help and errors go to stderr. Separate stdout and stderr when scripting. Exit `0` means success/help/version; `2` means a CLI usage error (unknown command/flag, missing CLI-required option, unexpected positional argument, invalid version arguments); `1` means another failure, including config, authentication, API, filesystem, cancellation, or init cancellation. Some required values are checked by the API client and therefore produce `1`, not `2`. A nonzero pull has no successful JSON result; check exit status before parsing stdout. Examples use placeholder IDs and credentials. Commands that create, edit, remove, invite, import, or set translations mutate remote data and run without a confirmation prompt. Help examples are safe locally. ## Common help and version options | Form | Type / required / default | Accepted values, repeatability, fallback | Example | | --- | --- | --- | --- | | `help`, `--help`, `-h` as command | Command; no options required | Prints top-level help; trailing arguments currently ignored; no environment fallback | `sentiary-cli help` | | `COMMAND --help` or `COMMAND -h` | Boolean help flag; optional, false | Built-in Go flag help; no fallback | `sentiary-cli pull --help` | | `version`, `--version`, `-v` as command | Command; no version argument | Prints version; optional exact `--json` second argument only | `sentiary-cli version` | | `version --json` | Literal switch; optional, off | Not repeatable; no environment fallback | `sentiary-cli version --json` | The table for each command lists every registered option. Each row lists its type, requirement, default, and example. Options have no environment fallback unless stated. Scalar options use the last supplied value; `pull --output` accumulates selections. All non-`pull` formats use the legacy four-format list. `get` needs exactly one of `--string-id` or `--key`. For `add`, language is optional; when supplied it triggers a second request to create the initial translation. The current client ignores `--translation` if language is omitted. A translation failure can leave the new term created. For `edit`, nonempty values update fields; `--clear-description` / `--clear-context` explicitly clear them and take precedence over the corresponding text flags. ## list-projects List projects. No command-specific options. Use `--help` for help. ``` sentiary-cli list-projects ``` ## get-project Get a project and membership. ``` sentiary-cli get-project ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## create-project Create a project. ``` sentiary-cli create-project --name=welcome ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--name` | Nonblank string | string; Required; `""` | `--name=welcome` | ## edit-project Rename a project. ``` sentiary-cli edit-project --name=welcome ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--name` | Nonblank string | string; Required; `""` | `--name=welcome` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## remove-project Remove a project and its data. ``` sentiary-cli remove-project ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## list-languages List all supported languages. ``` sentiary-cli list-languages ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--in-language` | Language ID for translated language names; empty uses authenticated user language | string; Optional; `""` | `--in-language=en` | ## list-project-languages List project languages. ``` sentiary-cli list-project-languages ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--in-language` | Language ID for translated language names; empty uses authenticated user language | string; Optional; `""` | `--in-language=en` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## add-project-language Add a language to a project. ``` sentiary-cli add-project-language --language=cs ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--language` | Exact API language ID (BCP 47 for project language management) | string; Required; `""` | `--language=cs` | | `--in-language` | Language ID for translated language names; empty uses authenticated user language | string; Optional; `""` | `--in-language=en` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## remove-project-language Remove a language and its translations. ``` sentiary-cli remove-project-language --language=cs ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--language` | Exact API language ID (BCP 47 for project language management) | string; Required; `""` | `--language=cs` | | `--in-language` | Language ID for translated language names; empty uses authenticated user language | string; Optional; `""` | `--in-language=en` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## list-project-members List project members. ``` sentiary-cli list-project-members ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## set-project-member-role Set a project member role. ``` sentiary-cli set-project-member-role --user-id=USER_ID --role=developer ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--user-id` | Existing Sentiary user ID | string; Required; `""` | `--user-id=USER_ID` | | `--role` | administrator, developer, translator | string; Required; `""` | `--role=developer` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## remove-project-member Remove a project member. ``` sentiary-cli remove-project-member --user-id=USER_ID ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--user-id` | Existing Sentiary user ID | string; Required; `""` | `--user-id=USER_ID` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## list-invitations List invitations for the current user. No command-specific options. Use `--help` for help. ``` sentiary-cli list-invitations ``` ## create-invitation Invite a user to a project. ``` sentiary-cli create-invitation --user-email=developer@example.com --role=developer ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--user-email` | Email address | string; Required; `""` | `--user-email=developer@example.com` | | `--role` | administrator, developer, translator | string; Required; `""` | `--role=developer` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## accept-invitation Accept an invitation. ``` sentiary-cli accept-invitation --invitation-id=INVITATION_ID ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--invitation-id` | Existing invitation ID | string; Required; `""` | `--invitation-id=INVITATION_ID` | ## decline-invitation Decline an invitation. ``` sentiary-cli decline-invitation --invitation-id=INVITATION_ID ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--invitation-id` | Existing invitation ID | string; Required; `""` | `--invitation-id=INVITATION_ID` | ## list-project-invitations List project invitations. ``` sentiary-cli list-project-invitations ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## remove-project-invitation Remove a project invitation. ``` sentiary-cli remove-project-invitation --invitation-id=INVITATION_ID ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--invitation-id` | Existing invitation ID | string; Required; `""` | `--invitation-id=INVITATION_ID` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## list List project strings. ``` sentiary-cli list --size=25 --include-language=en,cs --filter=NOT_TRANSLATED --filter-language=cs --order=UPDATED_DESC ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--size` | Integer; list sends only positive values, search normalizes nonpositive to 25 | int; Optional; `0` | `--size=25` | | `--cursor` | Opaque pagination cursor from prior response | string; Optional; `""` | `--cursor=CURSOR` | | `--include-language` | Comma-separated language IDs; trims items and omits empty ones | string; Optional; `""` | `--include-language=en,cs` | | `--filter` | ALL, TRANSLATED, NOT\_TRANSLATED; passed to API | string; Optional; `""` | `--filter=NOT_TRANSLATED` | | `--filter-language` | API language ID used by filter | string; Optional; `""` | `--filter-language=cs` | | `--order` | UPDATED\_ASC or UPDATED\_DESC; passed to API | string; Optional; `""` | `--order=UPDATED_DESC` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. Use `pagination.next` from the JSON response as the next `--cursor`; the CLI does not automatically fetch every page. ## search Search project strings. ``` sentiary-cli search --query=welcome --size=25 ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--query` | Nonblank search text | string; Required; `""` | `--query=welcome` | | `--size` | Integer; list sends only positive values, search normalizes nonpositive to 25 | int; Optional; `25` | `--size=25` | | `--cursor` | Opaque pagination cursor from prior response | string; Optional; `""` | `--cursor=CURSOR` | | `--include-language` | Comma-separated language IDs; trims items and omits empty ones | string; Optional; `""` | `--include-language=en,cs` | | `--filter` | ALL, TRANSLATED, NOT\_TRANSLATED; passed to API | string; Optional; `""` | `--filter=NOT_TRANSLATED` | | `--filter-language` | API language ID used by filter | string; Optional; `""` | `--filter-language=cs` | | `--order` | UPDATED\_ASC or UPDATED\_DESC; passed to API | string; Optional; `""` | `--order=UPDATED_DESC` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. Use `pagination.next` from the JSON response as the next `--cursor`; the CLI does not automatically fetch every page. ## get Get a string by ID or key. ``` sentiary-cli get --key=welcome --include-language=en,cs ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--string-id` | Existing term ID | string; Alternative to key; `""` | `--string-id=STRING_ID` | | `--key` | Exact key, mutually exclusive with string-id | string; Alternative to string-id; `""` | `--key=welcome` | | `--include-language` | Comma-separated language IDs; trims items and omits empty ones | string; Optional; `""` | `--include-language=en,cs` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## add Add a string. ``` sentiary-cli add --name=welcome --language=cs --translation=Ahoj ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--name` | Nonblank string | string; Required; `""` | `--name=welcome` | | `--description` | String; empty omits update in edit | string; Optional; `""` | `--description=Greeting` | | `--context` | String; empty omits update in edit | string; Optional; `""` | `--context=Home` | | `--language` | Exact API language ID (BCP 47 for project language management) | string; No; needed to apply initial translation; `""` | `--language=cs` | | `--translation` | Initial translation text; ignored without language | string; Optional; `""` | `--translation=Ahoj` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## edit Edit a string. ``` sentiary-cli edit --string-id=STRING_ID --clear-description ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--string-id` | Existing term ID | string; Required; `""` | `--string-id=STRING_ID` | | `--name` | Nonempty string updates name; empty leaves unchanged | string; Optional; `""` | `--name=welcome` | | `--description` | String; empty omits update in edit | string; Optional; `""` | `--description=Greeting` | | `--context` | String; empty omits update in edit | string; Optional; `""` | `--context=Home` | | `--clear-description` | true or false; true clears description | bool; Optional; `false` | `--clear-description=true` | | `--clear-context` | true or false; true clears context | bool; Optional; `false` | `--clear-context=true` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## remove Remove a string. ``` sentiary-cli remove --string-id=STRING_ID ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--string-id` | Existing term ID | string; Required; `""` | `--string-id=STRING_ID` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## set-translation Set a translation. ``` sentiary-cli set-translation --string-id=STRING_ID --language=cs --text=Ahoj ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--string-id` | Existing term ID | string; Required; `""` | `--string-id=STRING_ID` | | `--language` | Exact API language ID (BCP 47 for project language management) | string; Required; `""` | `--language=cs` | | `--text` | Translation text, including empty string | string; No (defaults to empty); `""` | `--text=Ahoj` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## remove-translation Remove a translation. ``` sentiary-cli remove-translation --string-id=STRING_ID --language=cs ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--string-id` | Existing term ID | string; Required; `""` | `--string-id=STRING_ID` | | `--language` | Exact API language ID (BCP 47 for project language management) | string; Required; `""` | `--language=cs` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## info Get project string information. ``` sentiary-cli info ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## export Export project strings. ``` sentiary-cli export --format=json --language=cs --output=strings.json ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--format` | json, android, apple, compose (trimmed and case-normalized) | string; Required; `""` | `--format=json` | | `--language` | Exact API language ID (BCP 47 for project language management) | string; Required; `""` | `--language=cs` | | `--include-empty` | true or false | bool; Optional; `false` | `--include-empty=true` | | `--output` | File path or - for stdout | string; Optional; `"-"` | `--output=strings.json` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## init Create a config for existing projects. ``` sentiary-cli init --directory=. ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--directory` | Existing target directory for init | string; Optional; `"."` | `--directory=.` | ## pull Download configured translation outputs. ``` sentiary-cli pull --output=android --output=compose --dry-run ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--config` | Config file path; empty searches current directory and parents | string; Optional; `""` | `--config=sentiary.toml` | | `--offline` | true or false; restore from one complete cached set | bool; Optional; `false` | `--offline=true` | | `--cache-dir` | Directory path; empty selects environment/OS cache; fallback: `SENTIARY_CACHE_DIR` | string; Optional; `""` | `--cache-dir=/tmp/sentiary-cache` | | `--dry-run` | true or false; emit preview without generated-file writes | bool; Optional; `false` | `--dry-run=true` | | `--json` | true or false; emit protocol result on successful non-preview pull | bool; Optional; `false` | `--json=true` | | `--integration-request` | Protocol 1 JSON file path; cannot combine with output selection | string; Optional; `""` | `--integration-request=request.json` | | `--force` | true or false; adopt existing files / discard edits | bool; Optional; `false` | `--force=true` | | `--output` | Defined output name; omit for all; duplicate selections fail | string list; Optional; `all outputs` | `--output=android` | For ownership, cache location, conditional requests, cleanup, and failure semantics, see [downloads](). For machine requests/results see [protocol](). `--dry-run` takes preview output precedence over `--json`. ## import Import project strings. ``` sentiary-cli import --format=json --language=cs --input=strings.json ``` | Option | Purpose and accepted values | Type / requirement / default | Example | | --- | --- | --- | --- | | `--project-id` | Existing project ID; fallback: `SENTIARY_PROJECT_ID` | string; Required after fallback; `""` | `--project-id=PROJECT_ID` | | `--format` | json, android, apple, compose (trimmed and case-normalized) | string; Required; `""` | `--format=json` | | `--language` | Exact API language ID (BCP 47 for project language management) | string; Required; `""` | `--language=cs` | | `--input` | File path or - for stdin | string; Optional; `"-"` | `--input=strings.json` | | `--override-existing` | true or false; true permits replacing existing translations | bool; Optional; `false` | `--override-existing=true` | The example assumes `SENTIARY_PROJECT_ID=PROJECT_ID`; `--project-id=PROJECT_ID` overrides it. ## Environment variables | Variable | Purpose and default | Example | | --- | --- | --- | | `SENTIARY_USER_API_KEY` | Account credential; unset by default. Required for API commands; highest-priority credential for online pulls. | `export SENTIARY_USER_API_KEY='YOUR_USER_API_KEY'` | | `SENTIARY_PROJECT_ID` | Project fallback for API commands; no default. | `export SENTIARY_PROJECT_ID='PROJECT_ID'` | | `SENTIARY_BASE_URL` | API server; defaults to `https://api.sentiary.com`. | `export SENTIARY_BASE_URL='https://api.staging.sentiary.coolify.matee.cz'` | | `SENTIARY_CACHE_DIR` | Export cache directory; defaults to the [OS cache path](). | `export SENTIARY_CACHE_DIR='.ci-cache/sentiary'` | | `SENTIARY_PROJECT_API_KEY` | Optional key offered by `init`; no default and no direct `pull` fallback. | `export SENTIARY_PROJECT_API_KEY='PROJECT_KEY'` | Installer-only `INSTALL_DIR` and `SENTIARY_VERSION` are covered in [installation](). ## JSON output for scripts `version --json` reports the CLI version and supported integration protocol versions. `pull --json` returns a versioned list of generated files and checksums on success. `pull --dry-run` returns a preview array, including when combined with `--json`. Check the exit status before reading JSON; errors go to stderr. `--integration-request` accepts a JSON file selecting outputs and validating resource roots. It cannot be combined with `--output`. Most CLI users do not need this option. Integration authors should use the [complete protocol]() and [JSON Schema bundle](). # Android, KMP Compose, and Xcode ``` sentiary-cli pull --output android --output compose --output ios --dry-run sentiary-cli pull --output android --output compose --output ios ``` Download the [mobile configuration]() as `sentiary.toml`. It defines one project, explicit `en`/`cs` languages, and three named outputs. Replace its project ID and project-key placeholder, or remove `api_key` and provide `SENTIARY_USER_API_KEY` instead. For automatic staging, use the [Gradle guide]() with this same TOML. The instructions below use the CLI before each application build. ## Android The `android` output writes `android/app/build/generated/sentiary/values/strings.xml` and `values-cs/strings.xml`. The map sends `en` to the unqualified default `values` folder and `cs` to `values-cs`; the API still receives the original IDs. ``` sentiary-cli pull --output android --dry-run sentiary-cli pull --output android ``` For manual integration, register the generated resource root in the Android module's `build.gradle.kts`: ``` android { sourceSets.getByName("main").res.srcDir("build/generated/sentiary") } ``` Run the CLI before Gradle in your local/CI build sequence. This snippet registers a directory; it does not add a download task dependency. After generation, build your app and check that its resource merge consumes the generated files. Do not register both generated and manually copied definitions of the same keys. ## KMP Compose The `compose` output writes under `shared/build/generated/sentiary/composeResources`. It uses the same explicit locale map, with `values/strings.xml` and `values-cs/strings.xml`. ``` sentiary-cli pull --output compose --dry-run sentiary-cli pull --output compose ``` For Compose Multiplatform versions exposing `customDirectory`, configure the shared module: ``` compose.resources { customDirectory( sourceSetName = "commonMain", directoryProvider = provider { layout.buildDirectory.dir("generated/sentiary/composeResources").get() }, ) } ``` Run the CLI before the Compose resource preparation/build tasks. Confirm this API against your installed Compose version; see the [Gradle requirements]() for plugin compatibility. The [Compose resource documentation]() describes custom resource directories. ## Combined Xcode catalog The `ios` output uses `xcstrings`, `combined`, and a fixed `ios/Resources/Localizable.xcstrings` path. It inherits `source_language = "en"` and both project languages. It creates one catalog, not one catalog per language. ``` sentiary-cli pull --output ios --dry-run sentiary-cli pull --output ios ``` Add the generated catalog to your Xcode target's resources. Ensure the file exists before the Xcode build. Set a source-language override on the output only when that language is included in the selection. `include_empty = true` writes empty localizations with state `new`; no fallback text is inserted. Do not edit generated text in Xcode expecting a merge back to Sentiary. Pull several outputs together with `--output android --output compose --output ios`, or omit selection for all. Offline restoration follows the same selection rules. The backend must support combined export and export/state protocol 1. # Configuration and offline use # Configuration reference ``` sentiary-cli init sentiary-cli pull --dry-run ``` The wizard writes `sentiary.toml`. Start from the [mobile configuration]() or [advanced configuration]() for an editable example. See [languages and aliases]() for wildcard and multi-project usage. ## Discovery and paths `pull --config PATH` resolves PATH relative to the current working directory. Without it, the CLI searches the working directory and then its parents for the nearest `sentiary.toml`. It does not merge ancestor configs. The containing directory is resolved through symlinks and becomes the root for relative destinations. Absolute destinations are accepted, subject to ownership and path checks. Changing the config filename also changes its ownership manifest name. All TOML fields are listed below. Unknown fields, invalid TOML, and unsupported types fail. There are no top-level global language defaults and no server URL field in TOML. The CLI defaults to `https://api.sentiary.com`; `SENTIARY_BASE_URL` selects a different API base URL (surrounding whitespace and trailing slashes are removed). ## Top level | Field | Type | Required | Default / accepted values | Example | | --- | --- | --- | --- | --- | | `config_version` | integer | Yes | Must be `1` | `config_version = 1` | | `projects` | table of named project tables | Yes for referenced projects | Aliases must be nonempty | `[projects.mobile]` | | `outputs` | table of named output tables | Yes | At least one output; names must be nonempty | `[outputs.android]` | ## Project: `[projects.NAME]` | Field | Type | Required | Default / behavior | Example | | --- | --- | --- | --- | --- | | `id` | string | Yes | Nonempty existing Sentiary project ID | `id = "PROJECT_ID"` | | `api_key` | string | Online if no user key | Empty when omitted; project credential; lower priority than either user-key source | `api_key = "PROJECT_KEY"` | | `languages` | array of strings or string `"*"` | Unless each output supplies it | Nonempty unique IDs, or all project languages | `languages = ["en", "cs"]` | | `source_language` | string | For combined output unless overridden | No default | `source_language = "en"` | | `include_empty` | boolean | No | `false` | `include_empty = false` | ## Output: `[outputs.NAME]` | Field | Type | Required | Default / behavior | Example | | --- | --- | --- | --- | --- | | `project` | string | Yes | Defined project alias, not its remote ID | `project = "mobile"` | | `format` | string | Yes | Exact token from the format table below | `format = "android"` | | `layout` | string | No | `per_language`; also `combined` for `xcstrings` | `layout = "per_language"` | | `path` | string | Yes | Nonempty destination or template | `path = "build/resources/{locale}/strings.xml"` | | `languages` | array of strings or `"*"` | No if project defines it | Replaces project selection in full | `languages = ["en", "cs"]` | | `source_language` | string | For combined unless inherited | Replaces project source; ignored for per-language files | `source_language = "en"` | | `include_empty` | boolean | No | Replaces project value, including explicit `false` | `include_empty = false` | | `locale_map` | table of string values | When path contains `{locale}` | Empty map; target language ID to path component; no automatic mapping | `locale_map = { en = "values" }` | | `language_aliases` | table of string values | No | Empty map; target ID to source language ID; chains allowed | `language_aliases = { en-GB = "en" }` | `languages`, `source_language`, and `include_empty` inherit independently from the project. No list or map merging occurs. `locale_map` and `language_aliases` belong only to outputs. Missing languages fail; an empty array is not shorthand for all languages. `{language}` expands to the exact selected target ID. `{locale}` expands using that target's locale map entry. No locale normalization happens automatically. IDs such as `cs`, `cs-CZ`, and `cs_CZ` are distinct. Use your project's actual IDs. For Android qualifiers such as `values-en-rGB` or Compose resource qualifiers, supply an explicit map that matches the resource consumer. Aliases copy a complete exported file; they do not fill individual missing keys. For `en-AU → en-GB → en`, the target filename uses `en-AU` while the API/cache language is `en`. Every chain must end in a language returned by the project. Cycles, missing sources, and invalid IDs fail, including aliases not selected for output. Explicit selections include only listed target IDs; wildcard selections add all alias targets to all project languages. ## Formats and layouts Every token below supports `per_language`: There is no default format; set `format` explicitly. All formats default to `per_language` layout. Set the output `path` explicitly, for example: | Format | Use | Example `path` | | --- | --- | --- | | `json` | Sentiary JSON | `"build/locales/{language}.json"` | | `key_value_json` | Flat key-value JSON | `"build/locales/{language}.json"` | | `i18next` | i18next translations | `"build/locales/{language}.json"` | | `poeditor_json` | POEditor JSON | `"build/locales/{language}.json"` | | `arb` | Application Resource Bundle | `"build/locales/{language}.arb"` | | `android` | Android string resources | `"build/resources/{locale}/strings.xml"` | | `compose` | Compose string resources | `"build/resources/{locale}/strings.xml"` | | `apple` | Apple strings | `"build/locales/{language}.strings"` | | `xcstrings` | Xcode string catalog | `"build/locales/{language}.xcstrings"` | | `stringsdict` | Apple string dictionary | `"build/locales/{language}.stringsdict"` | | `resw` | Windows resources | `"build/locales/{language}.resw"` | | `resx` | .NET resources | `"build/locales/{language}.resx"` | | `po` | Gettext translations | `"build/locales/{language}.po"` | | `pot` | Gettext template | `"build/locales/{language}.pot"` | | `mo` | Compiled Gettext | `"build/locales/{language}.mo"` | | `csv` | Comma-separated values | `"build/locales/{language}.csv"` | | `ini` | INI translations | `"build/locales/{language}.ini"` | | `properties` | Java properties | `"build/locales/{language}.properties"` | | `ts` | Qt translation source | `"build/locales/{language}.ts"` | | `yml` | YAML translations | `"build/locales/{language}.yml"` | | `xliff` | XLIFF interchange | `"build/locales/{language}.xliff"` | | `xlf` | XLF interchange | `"build/locales/{language}.xlf"` | | `xliff_1_2` | XLIFF 1.2 interchange | `"build/locales/{language}.xliff"` | | `rise_360_xliff` | Rise 360 XLIFF | `"build/locales/{language}.xliff"` | | `xmb` | XML message bundle | `"build/locales/{language}.xmb"` | | `xtb` | XML translation bundle | `"build/locales/{language}.xtb"` | | `xls` | Excel binary spreadsheet | `"build/locales/{language}.xls"` | | `xlsx` | Excel workbook | `"build/locales/{language}.xlsx"` | Only `xcstrings` supports `combined`. That layout creates one catalog for all selected languages. It requires a fixed path, no placeholders, no `locale_map`, no `language_aliases`, and a source language included in the resolved selection. Combined catalogs support plain strings; plurals and device variants are outside the current exporter contract. `include_empty = false` omits empty translations. For combined catalogs, `true` includes empty localizations with state `new`; it does not invent fallback text. `pull` preserves downloaded bytes, including binary spreadsheet/Gettext exports. Legacy `export` and `import` accept only `json`, `android`, `apple`, and `compose`. ## Local supplement and credentials `sentiary.local.toml` is read beside the selected shared config, even when that config has another filename. It accepts one optional field, `user_api_key` (string, default empty), for example `user_api_key = "YOUR_USER_API_KEY"`. Copy the [local credential template]() to that filename and replace its placeholder; keep it out of source control. Unknown fields fail. It cannot override projects, outputs, server, or paths. Online credentials for every project are selected in this order: 1. Nonblank `SENTIARY_USER_API_KEY`. 2. Nonblank local `user_api_key`. 3. That project's nonblank `api_key`. An invalid selected key fails without falling back to another. All configured projects require credentials during online config loading, including projects with no selected output. `SENTIARY_PROJECT_API_KEY` is an `init` input only; `pull` does not read it directly. `SENTIARY_PROJECT_ID` applies to legacy API commands, not TOML project IDs. Legacy API commands do not load TOML or local credentials. Offline loading ignores the local supplement entirely and permits missing credentials. Cache data contains translation content even though it contains no keys. Ignore `sentiary.local.toml` and `.sentiary/` in version control. Shared `api_key` retains the project's existing permissions; it is not a download-only key. ## Validation failures Language IDs must be nonblank and unique. They cannot be `.`, `..`, or contain slash, backslash, braces, or NUL. Unknown path expressions and NUL paths fail. Missing locale entries fail when planning the resolved targets. Duplicate output selections, unknown output names, and combined catalogs whose source is not selected fail. Destinations must be distinct ignoring case, and no destination may be another destination's parent directory. Case-only renames of owned paths fail. Config files, local credentials, `.sentiary/`, and symbolic links in destination paths are protected. A preview validates the download plan, but ownership and filesystem protections are checked during the actual write workflow. See [failure recovery](). # Generic exports and multiple projects/modules ``` sentiary-cli pull --output module_a --output module_b --dry-run sentiary-cli pull --output module_a --output module_b ``` Download [advanced TOML]() and save it as `sentiary.toml`. Replace both project IDs and supply a user key. It contains no credentials. ## Wildcards and locale mappings The `app` project uses `languages = "*"`. `module_a` overrides that with explicit target languages, so it generates only those files. Its locale map chooses resource folder names independently of API IDs. Unknown language IDs fail at the service or alias resolver; they are not automatically converted. `module_b` inherits wildcard selection and uses `{language}` in generic JSON filenames. This avoids having to enumerate a locale map as project languages change. The wildcard records the server's language list in the completed set; offline mode reuses that list. ## Chained aliases `module_a` maps `en-AU` to `en-GB`, then `en-GB` to `en`. Both target files contain the whole English export. The project must contain `en`, and all configured alias chains must resolve even if a target is not selected. No per-key fallback occurs. ``` sentiary-cli pull --output module_a --dry-run sentiary-cli pull --output module_a ``` ## Multiple Gradle modules `module_a` writes under `feature-a/build/generated/sentiary`; register that resource root in module `feature-a` using the Android setup in the [mobile guide](). `module_b` writes JSON under `feature-b/build/generated/locales`; configure that module's consumer to load those files as its application requires. A `.json` extension does not make it an Android or Compose resource. Run one CLI command before both module builds: ``` sentiary-cli pull --output module_a --output module_b ``` This is manual orchestration. The Gradle 2.0 settings plugin can replace manual orchestration for [Android/Compose bindings](). Keep output paths distinct across modules and avoid two configs managing the same destinations. ## Multiple remote projects and generic formats The `website` output uses a second project and explicitly selects one language. A single user key must have access to both configured projects. Alternatively, remove user-key sources and add appropriate project keys to each project table; do not publish real keys. `catalog` overrides both source language and language selection for a combined Xcode catalog. It also enables empty translations. `sheet` exports binary XLSX bytes to language-specific files; use `pull` rather than legacy `export` for that format. ``` sentiary-cli pull --output website --output catalog --output sheet ``` Review the [configuration rules]() before adding locale maps, combined layouts, or new formats. # Downloads and offline use ``` sentiary-cli pull sentiary-cli pull --offline ``` Run online first to save a complete set of exports. Offline mode restores that set without credentials or a network connection. ## Ownership and adoption Each config uses `.sentiary/.json` to record output names, absolute file paths, and SHA-256 checksums. A missing generated file can be restored. An existing untracked destination or a manually changed owned file blocks overwrite or removal, even when its content happens to match the new export. > [!WARNING] > > **Replacing existing files** > > Review existing files and the preview, then use `sentiary-cli pull --force` to adopt destinations or discard edits. `--force` does not bypass invalid paths, symlink protection, case-only rename restrictions, or ownership of an unselected output. Never expect manual Xcode catalog edits to merge into server translations. After a successful pull, obsolete owned files for selected outputs are removed. Unrelated files and unselected outputs remain. A full pull also cleans ownership for removed output definitions; a selected pull limits cleanup to those names. Empty directories may remain. ## Concurrent pulls and write failures Pulls for the same configuration wait for one another. Cancellation stops the wait. A leftover lock file does not prove a pull is running; do not delete it while another pull may be active. Different configs must not manage the same destinations. All requested exports must succeed before any generated file changes. Individual file replacement is atomic, but the whole pull is not a multi-file transaction. An I/O failure can leave some files updated; inspect affected files before retrying with `--force`. Online pulls authenticate and check for updates. A server, access, or download error never silently selects offline data. A completed export set may remain available even if a later destination write fails. Consistency is per project; a pull is not a cross-project transaction or a translation lockfile. ## Cache location and identity Precedence: `--cache-dir`, then `SENTIARY_CACHE_DIR`, then the OS cache directory shown below. Relative overrides are relative to the process working directory. | OS | Usual default base | | --- | --- | | macOS | `$HOME/Library/Caches/sentiary/exports-v1` | | Linux | `$XDG_CACHE_HOME/sentiary/exports-v1`, otherwise `$HOME/.cache/sentiary/exports-v1` | | Windows | `%LocalAppData%\sentiary\exports-v1` | Cached exports are scoped to the server, project, format, layout, languages, source language, and empty-translation policy. Moving a checkout or renaming outputs can reuse exports. Sets do not expire automatically. Protect cached translation content like generated files; it contains no API keys. ## Offline restoration `pull --offline` makes no network calls and needs no credentials. It finds the newest single completed set covering the requested exports, including a selected subset. It does not combine several sets. Wildcard expansion uses that set's saved project languages. Every selected object's checksum must verify before writes. Completion time and age are printed to stderr. Deleting `build/` does not invalidate a cache set. Restore it with the same config and cache location. Changing format, selected languages, source language, or empty-value policy may require another online pull. Offline mode does not establish current access or freshness. ## Failure recovery | Failure | Recovery | | --- | --- | | Untracked or edited destination | Review/back up files; deliberately adopt with `--force` | | Case-only owned rename | Move through a distinct destination in a successful pull | | Unsupported export/state protocol | Use a compatible backend; legacy commands remain separate | | Project changed during three attempts | Retry after edits settle | | No completed set covers request | Run an online pull for that selection and cache location | | Missing/corrupt object | Run online to fetch valid content; do not edit hashes | | Corrupt cache metadata | Preserve for diagnosis, choose a fresh cache directory, then pull online | | Partial destination write or manifest save | Inspect the affected files; retry with `--force` only after accepting replacements | | Invalid ownership manifest | Preserve it and inspect previous owned paths before rebuilding ownership; unrecorded obsolete files cannot be cleaned automatically | A cache publication failure leaves destinations unchanged and retains the previously published set where possible. A later destination error leaves the completed set available for recovery. See the recovery steps below. ## CI caching Choose a cache path explicitly for portable CI. Cache the CLI's exports directory separately from generated `build/` directories. Do not treat a restored cache as proof of current authorization or server freshness; an online pull still authenticates and checks revisions. A GitHub Actions sequence, after installing the CLI and checking out your application's config: ``` - uses: actions/cache@v4 with: path: .ci-cache/sentiary key: sentiary-${{ runner.os }}-${{ hashFiles('sentiary.toml') }}-${{ github.run_id }} restore-keys: | sentiary-${{ runner.os }}-${{ hashFiles('sentiary.toml') }}- sentiary-${{ runner.os }}- - name: Download translations env: SENTIARY_USER_API_KEY: ${{ secrets.SENTIARY_USER_API_KEY }} SENTIARY_CACHE_DIR: .ci-cache/sentiary run: sentiary-cli pull ``` The run-specific key permits saving refreshed sets; restore prefixes reuse previous content. Restrict access to cached translations as appropriate for your project. Do not include credentials or `sentiary.local.toml` in cache paths. Ignore `.ci-cache/` locally if using this convention. Pin the CLI release separately. ## Restore deleted generated files With the mobile config, prime Android and Compose together: ``` sentiary-cli pull --cache-dir .ci-cache/sentiary --output android --output compose ``` Delete only the generated Android and Compose directories using your normal clean command or file manager. Keep `.sentiary/` and `.ci-cache/sentiary/`. Then restore: ``` sentiary-cli pull --offline --cache-dir .ci-cache/sentiary --output android --output compose ``` The command restores missing files without a key or network. Verify the resulting paths with your app build. It prints the completed set's timestamp and age to stderr. A cache primed only for Android cannot restore Compose. Separate completed sets are not combined; prime the joint selection online first. For a planned disconnected CI build, use the same explicit offline invocation after restoring a trusted complete cache. Do not automatically retry a failed online pull offline: doing so can conceal revoked access or service errors. The 2.0 settings plugin forwards Gradle's `--offline` to the CLI. Standalone CLI invocations still need the explicit `--offline` flag. # Gradle # Gradle plugin 2.0 ## Setup Apply the plugin once in root `settings.gradle.kts`: ``` pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id("com.sentiary.gradle") version "2.0.0" } ``` Create `sentiary.toml` with the [first download guide](), then follow the [Android and Compose guide](). Download the [mobile settings]() to configure bindings. [Plugin source on GitHub]() TOML owns project IDs, credentials, output formats/paths, language selection, aliases, and empty-value policy. The settings extension owns only resource consumption. There is no compatibility layer for the old `projectId`, `projectApiKey`, `exportPaths`, or naming callbacks. ## Binding containers and every field The settings `sentiary` extension has two named containers: `android { create("NAME") { ... } }` and `compose { create("NAME") { ... } }`. Container kind supplies protocol format `android` or `compose`; the named output must use that format and `per_language` layout. A generic or combined Xcode output cannot be bound as Android/Compose resources. | Field | Type | Required | Default | Behavior / accepted values | Example | | --- | --- | --- | --- | --- | --- | | Binding name (`create`) | string | Yes | None | Name within its container; identifies errors, not an output selection by itself | `create("app")` | | `modulePath` | string | Yes | Empty (invalid) | Absolute Gradle project path beginning with `:`, referring to an included module | `modulePath = ":app"` | | `output` | string | Yes | Empty (invalid) | Nonblank named TOML output, validated by CLI | `output = "android"` | | `sourceSet` | string | No | `main` | Matches `[A-Za-z][A-Za-z0-9_]*`; use `commonMain` explicitly for KMP Compose | `sourceSet = "commonMain"` | | `resourceRoot` | string | Yes | Empty (invalid) | Relative to build root, canonicalized; generated files must be strictly inside this root | `resourceRoot = "app/build/generated/sentiary"` | | `localDirectories` | list of strings | No | Empty list | Compose-only extra local resource roots, relative to build root; Android requires empty list | `localDirectories = listOf("shared/extraComposeResources")` | There is at most one binding for each `(modulePath, kind, sourceSet)`. The same output may be selected by more than one binding if each root contains its files. Additional binding fields, format overrides, variant selectors, or language/credential overrides do not exist. Android needs `com.android.application` or `com.android.library` in the bound module. Source sets beginning with `test` or `androidTest` (case-insensitive) are rejected. The adapter matches the selected source-set name against `main`, variant name, build type, combined flavor name, and individual flavor names. A source-set name's syntax alone does not prove that a variant will consume it. Compose needs `org.jetbrains.compose`. Its adapter uses the public Compose `customDirectory` API and stages the conventional `src//composeResources` plus any `localDirectories`. Extra roots must have the same resource directory structure as ordinary Compose resources. ## Overrides and credentials These are Gradle project properties, supplied with `-P` or `gradle.properties`. They are not extension fields. Standard Gradle property resolution applies; command-line `-P` overrides file values. No plugin-specific environment fallback is implemented for these properties. | Property | Type | Required | Default | Behavior / example | | --- | --- | --- | --- | --- | | `sentiary.config` | string path | No | Empty | CLI discovery from build root; `-Psentiary.config=config/sentiary.toml` uses that path | | `sentiary.cliVersion` | string release tag | No | `v2.0.0` | Managed version; `[A-Za-z0-9][A-Za-z0-9._-]*`; e.g. `-Psentiary.cliVersion=v2.0.0` | | `sentiary.cliExecutable` | string path | No | Empty | Overrides managed CLI installation; absolute path or relative to build root, e.g. `-Psentiary.cliExecutable=/path/to/sentiary-cli` | An executable override takes precedence over `cliVersion` and bypasses managed-download checksum verification, but still undergoes protocol negotiation. There is no public checksum, installer URL, or CLI-install-cache directory override. `SENTIARY_USER_API_KEY`, local credentials, `SENTIARY_CACHE_DIR`, and `SENTIARY_BASE_URL` are inherited by the CLI process. The export cache is separate from the managed binary cache. TOML destinations remain relative to the config directory even when `sentiary.config` points below the build root; binding roots remain relative to the build root. ## Tasks and automatic build dependencies | Task | Behavior | | --- | --- | | Root `:sentiaryPull` | Runs version handshake, invokes the CLI, and downloads the selected outputs | | `sentiaryStageAndroid` in bound module | Depends on root pull; validates checksums/root containment; stages generated and source-set local resources | | `sentiaryStageCompose` in bound module | Depends on root pull; stages generated, conventional, and extra Compose roots | Names capitalize the first character of the source set: `main` becomes `sentiaryStageAndroidMain`, `commonMain` becomes `sentiaryStageComposeCommonMain`. Staging outputs go to the module's `build/sentiary/staging//`. When bindings exist, the shared CLI request selects only their outputs; unbound outputs are excluded. With no bindings, `sentiaryPull` runs a normal full-config pull, useful for generic exports or Xcode. A build runs the shared task at most once in its task graph. The task is deliberately not cached or skipped for online freshness, even when Configuration Cache is reused. Staging also runs again to validate current files/collisions. Old `sentiaryUpdateLocalizations`, `sentiaryUpdateProjectInfo`, and `--force-update` are removed; no plugin `--force` option forwards ownership adoption. Perform deliberate adoption with the CLI. Android source-set resource directories are collected into staging, then replaced by the staged directory. Applicable variant pre-build lifecycle tasks depend on staging. Compose registers a task-backed custom directory. Do not add separate manual directory wiring for the same binding. Keep framework source-set configuration and the plugin's generated roots consistent. ## Collisions and staging guarantees Within a binding, distinct physical files with the same relative path, compared without case, fail even when content is identical. A repeated reference to the same canonical physical file is deduplicated. Rename local resources such as `values/strings.xml` when they collide with a generated file at that relative path; the plugin does not merge XML or implement key-level fallback. Android layering across distinct source sets remains the framework's responsibility. Generated checksums and resource-root containment are checked before replacement. Missing generated resources and non-directory local inputs fail. Inputs and collisions are validated before replacing the old staged tree. Final directory copying is not a filesystem transaction; an I/O error can still leave incomplete staging. Re-run after fixing the failure. ## Managed CLI and offline builds The plugin installs the CLI for macOS, Linux, and Windows on AMD64/ARM64. It verifies the release's `SHA256SUMS` before installation and checks cached binaries before reuse. Managed binaries live in `GRADLE_USER_HOME/caches/sentiary/cli///`. A cached binary is checked against its saved checksum manifest before reuse. With Gradle `--offline`, a missing/corrupt managed binary fails instead of downloading; use an intact primed binary or `sentiary.cliExecutable`. Gradle `--offline` forwards CLI `--offline`. It also needs a complete matching export-cache set. After deleting generated files, the pull task restores them, then staging runs again. Prime both caches online first. No automatic online-error-to-offline fallback exists. See [CI and offline recovery](). ## Requirements | Component | Version | | --- | --- | | Gradle | 9.1.0 or newer | | Java | 21 | | Android Gradle Plugin | 9.0.0 (compile SDK 36) | | Kotlin for consuming projects | 2.3.20 | | Compose Multiplatform | 1.10.3 | | Sentiary CLI | 2.0, integration protocol 1 | Use this supported framework combination; other framework versions are not covered by this guide. Configuration Cache and Isolated Projects are supported. Online pulls still run to check freshness when Gradle reuses configuration. For Isolated Projects with Compose 1.10.3, set `org.jetbrains.compose.hot.reload.disable=true`. Set `kotlin.internal.collectFUSMetrics=false` to avoid a Kotlin statistics build-service classloader conflict. Both are framework properties, not Sentiary extension fields. Download the [Gradle properties example](). ## Limitations There is one binding per source set/kind/module. Android supports application/library plugins and production source sets only. There is no generic/combined resource adapter or public managed-checksum override. See [migration]() for removed 1.x options and [offline use]() for recovery requirements. # Android and Compose builds ``` ./gradlew sentiaryPull ./gradlew :android:app:mergeDebugResources :shared:prepareComposeResourcesTaskForCommonMain ``` Configure your existing Android/KMP build as follows. See the [reference]() for every binding and override. ## Android and KMP Compose 1. Copy [mobile TOML]() as `sentiary.toml`. Replace its project ID/key and verify language IDs. 2. Merge the [canonical mobile settings]() into your root `settings.gradle.kts`. Keep the required settings block order, repositories, and your existing modules. The example assumes modules `:android:app` and `:shared`. 3. Ensure the app module applies `com.android.application` (or `com.android.library`) and the shared module applies `org.jetbrains.compose` with its existing KMP/Compose plugins. Use the [supported versions](). 4. Provide credentials through the [CLI environment or local TOML](). The plugin manages the CLI installation. 5. Run a consuming resource task or `sentiaryPull` as shown below. ``` ./gradlew sentiaryPull ./gradlew :android:app:mergeDebugResources :shared:prepareComposeResourcesTaskForCommonMain ``` The shared task selects only the bound `android` and `compose` outputs. It does not download the unbound `ios` catalog. Staging uses the generated root from each binding, preserves local resources at distinct relative paths, and wires task dependencies automatically. Do not also apply the manual resource-directory wiring from the separate mobile guide. The Android adapter collects the source set's configured resource directories. The Compose adapter includes `shared/src/commonMain/composeResources` automatically. To include extra local Compose roots, set `localDirectories = listOf("shared/extraComposeResources")` inside that binding. Those paths are relative to the build root and must have a standard Compose resource tree. A duplicate relative filename across generated/local roots fails instead of overriding. See [collision rules](). ## Multiple modules Copy [multi-module TOML]() as `sentiary.toml`, and merge [multi-module settings]() into your existing settings. Replace project IDs. The app module applies `com.android.application`; the library applies `com.android.library`. Both need ordinary Android namespace/SDK configuration. The example binds `app_main`, `app_debug`, and `library_main` to `:app` main/debug and `:library` main. Main and debug use separate generated roots. Android's ordinary source-set priority applies across layers; files colliding inside one layer fail during staging. All three bindings belong to one shared request, even when only one module resource task is requested. ``` ./gradlew :app:mergeDebugResources :library:packageDebugResources --parallel ``` Use a user key with access to both remote projects, or project keys with no higher-priority user key configured. Add more bindings for additional modules/source sets, keeping at most one per kind/module/source set. The same mechanism supports multiple Compose modules by creating one `compose` binding per module and matching each output/root; use `commonMain` explicitly. ## Offline clean recovery and configuration reuse Run the consuming tasks online first to prime a complete export set (and the binary cache if using managed installation). Delete only generated directories and module staging outputs. Keep the export cache and an intact CLI binary. Then use the same consuming tasks with Gradle `--offline`; it forwards the offline flag, restores generated files, and stages resources again. ``` ./gradlew sentiaryPull --offline ``` Run consuming tasks twice with `--configuration-cache` and `-Dorg.gradle.unsafe.isolated-projects=true`. Check for configuration reuse and one pull per task graph. Online pulls still run again to check freshness; configuration reuse does not skip them. ## Generic exports and Xcode For a build that only needs generic/Xcode files, apply the settings plugin with no resource bindings. `sentiaryPull` then downloads all configured outputs without Android/Compose adapters. A mixed build with resource bindings downloads only bound outputs; use a separate explicit CLI selection for an unbound catalog: ``` sentiary-cli pull --output ios ``` Run that before the Xcode build and register the catalog in its target. It is not a Gradle resource staging binding. See [CI caching]() for export-cache storage and [migration]() for old-task and ownership differences. # API # REST API reference Explore endpoints, request parameters, responses, and models from the deployed API with Scalar API Reference. [Open API reference]() [Download the OpenAPI schema]() The production API base URL is `https://api.sentiary.com/api/v1`. For staging, use `https://api.staging.sentiary.coolify.matee.cz/api/v1`. The schema download on this site follows the documentation environment. Its default server is production; select the staging base URL explicitly when working with staging. The reference is read-only. Use your API client to make requests. Account and project API keys use `Authorization: Bearer ` on supported routes. Account permissions and project membership still apply. Routes that require a Firebase session use `Authorization: Bearer `. Check each route's authentication requirements. The reference follows the deployed API. Download the schema from the same environment as the API you use. # Migration and troubleshooting # Migrate from Gradle plugin 1.x to 2.0 ``` sentiary-cli pull --dry-run ``` Preview your converted configuration before replacing generated files. Gradle 2.0 uses a settings plugin and CLI-managed downloads. Follow the [setup and requirements](). | 1.x concept | New workflow | | --- | --- | | Project plugin in module/root `build.gradle.kts`, possibly convention plugins | One `com.sentiary.gradle` settings plugin in root `settings.gradle.kts` | | `projectId` | `[projects.NAME].id` | | `projectApiKey`, Gradle/local properties | User-key environment/local TOML, or project `api_key`; review changed precedence | | `exportPaths` | Named `[outputs.NAME]` entries plus separate settings module/source-set/resource bindings | | `Format.Android`, `ComposeResources`, `Json`, `Apple` | TOML `android`, `compose`, `json`, `apple` | | `defaultLanguage` | Explicit `locale_map` for resource folder naming; combined `source_language` is a separate semantic choice | | Folder/file naming callbacks | Explicit `path`, `{language}` / `{locale}`, and `locale_map`; arbitrary callbacks do not transfer | | `disabledLanguages` | Explicit allowed `languages` list; wildcard has no exclusion syntax | | `languageOverrides.fallbackTo` | `language_aliases` target-to-source map; chains allowed, whole-file copies only | | `caching.enabled`, project-info timestamp cache | CLI conditional exports and completed sets; no equivalent disable-cache field | | `requestTimeoutMillis` | No TOML/CLI timeout override; current executable uses a 30-second HTTP client | | `--force-update` | No direct equivalent; online pull always checks state. `--force` instead discards local edits/adopts ownership | | `sentiaryUpdateProjectInfo` / old update task | `:sentiaryPull`; resource staging adds its dependency automatically | ## Prepare shared configuration Copy the [mobile example]() and replace IDs, languages, and paths. Convert every naming callback to an explicit destination rule. Use [advanced configuration]() for aliases, wildcard selection, output overrides, and multiple projects. Combined catalogs require a selected source language and cannot use aliases or locale mappings. Move personal credentials out of Gradle scripts. Online TOML precedence is user environment key, local user key, then shared project key. `SENTIARY_PROJECT_API_KEY` is not a direct pull fallback, and legacy API commands do not read local TOML. Check which credential will win before migration. ## Review generated-file ownership Run `pull --dry-run` first. Existing files written by 1.x are untracked by the CLI even if unchanged. Back them up or review them, then adopt deliberately with `pull --force`. Use generated build directories where possible. Verify that no old task continues writing the same destinations. Run an online pull to prime a completed set, then validate offline recovery after deleting only generated files. Preserve the cache and ownership state. A Gradle build-cache hit and the CLI export cache solve different problems. ## Switch Gradle wiring Replace old module/convention-plugin application, old task dependencies, credential properties, and `exportPaths` with the [settings DSL](). Register each Android production source-set layer separately. Compose keeps conventional local resources plus explicit additional directories; do not register another `customDirectory` for a bound source set. No legacy downloader remains. # Troubleshooting | Symptom | What to check | | --- | --- | | `command not found` | Installation directory and shell PATH; inspect resolved executable | | Unknown `pull` or `init` command | Install CLI 2.0; inspect version and command help | | No `sentiary.toml` found | Run inside its directory tree or pass `--config` | | Invalid TOML or unsupported fields | Exact field names, types, and `config_version = 1`; local supplement allows only `user_api_key` | | Unexpected credential used | User environment key wins over local user key, then project key; invalid keys do not fall back | | Legacy command ignores local key | Legacy API commands read `SENTIARY_USER_API_KEY`, not TOML | | Wrong language folder | Locale mapping changes paths only; use exact remote language IDs and explicit consumer-compatible qualifiers | | Alias source missing | Every chain must end in a project language; aliases copy whole files | | Combined catalog rejected | Use `xcstrings`, fixed path, selected source, no locale map/aliases/placeholders | | Binary export needed | Use configured `pull`; legacy `export` accepts only four text-format tokens | | Existing destination rejected | Review file and use deliberate adoption with `--force` | | Pull appears to wait | Another process may hold the per-config or cache lock; lock-file presence is not proof | | Offline cache miss | Cache location, selected requests, saved language expansion, intact single completed set | | Online authorization/server failure | Resolve online failure; there is no automatic offline fallback | | Files missing after clean | Restore explicitly using `pull --offline` and the same completed cache set | For path conflicts, corrupt metadata, partial writes, and stale sets, follow [download failure recovery](). For syntax, types, defaults, and required options, see [CLI reference](). # MCP and integration # Hosted MCP The hosted frontend reads the user API key when it creates an MCP session. Each session gets a separate API client. The hosted frontend reads the key from the first available source: 1. `X-Sentiary-User-Api-Key: ` 2. `Authorization: Bearer ` 3. `SENTIARY_USER_API_KEY` in the service environment The `Authorization` header can also contain a bare API key. Example MCP client configuration: ``` { "mcpServers": { "sentiary": { "type": "http", "url": "https://mcp.example.com/", "headers": { "Authorization": "Bearer user-api-key" } } } } ``` The hosted frontend uses port `8080` by default. Set `PORT` to use a different port. The service exposes MCP at `/` and its health check at `/healthz`. ## Self-host with Docker From the repository root, build and run the hosted frontend: ``` docker build -f go/Dockerfile -t sentiary-mcp . docker run --rm -p 8080:8080 sentiary-mcp ``` Clients can send their own API keys. The container does not need a shared API key. ## MCP tools Project tools: - `list_projects` - `get_project` - `create_project` - `edit_project` - `remove_project` Language tools: - `list_supported_languages` - `list_project_languages` - `add_project_language` - `remove_project_language` Member tools: - `list_project_members` - `set_project_member_role` - `remove_project_member` Invitation tools: - `list_invitations` - `create_invitation` - `accept_invitation` - `decline_invitation` - `list_project_invitations` - `remove_project_invitation` String tools: - `list_project_strings` - `search_project_strings` - `get_project_string` - `get_project_string_by_key` - `add_project_string` - `edit_project_string` - `remove_project_string` - `set_project_string_translation` - `remove_project_string_translation` Batch tools: - `get_project_strings_info` - `export_project_strings` - `import_project_strings` The batch tools support `json`, `android`, `apple`, and `compose` formats. # CLI integration protocol This machine interface is separate from ordinary CLI commands and the backend export/state protocol. The [Gradle settings plugin]() uses it for resource bindings. Download the [JSON Schema bundle]() for handshake, request, result, and preview shapes. Use a reference such as `schemas.json#/$defs/request`, `#/$defs/result`, `#/$defs/preview`, or `#/$defs/handshake` to select a shape from the bundle. The bundle describes emitted/accepted structure; semantic restrictions below also apply. Paths and digests in examples are illustrative. ## Version handshake ``` sentiary-cli version --json ``` ``` {"version":"dev","integrationProtocols":[1]} ``` `version` is a string; `integrationProtocols` is an array of supported integer versions. The current executable advertises `[1]`. Negotiate support before running a request; do not infer it solely from a release version. `--version --json` and `-v --json` also work. Extra version arguments return usage exit 2. ## Request ``` { "protocolVersion": 1, "bindings": [ {"output":"android","format":"android","resourceRoot":"/workspace/android/app/build/generated/sentiary"} ] } ``` Run `sentiary-cli pull --config /workspace/sentiary.toml --integration-request request.json --json`. | Field | Type | Contract | | --- | --- | --- | | `protocolVersion` | integer | Required, must equal `1` | | `bindings` | array of objects | Selects named outputs; absent/null/empty currently selects all outputs with no root validation | | `bindings[].output` | string | Existing output name; repeated names deduplicate selection | | `bindings[].format` | string | Must equal output format, and output layout must be `per_language` | | `bindings[].resourceRoot` | string | Absolute path; every generated file for that binding must be strictly beneath it | Unknown fields, malformed JSON, a second JSON value, unsupported versions, unknown outputs, nonabsolute roots, mismatched format/layout, and files outside the root fail. Repeated bindings are each validated. `--output` and `--integration-request` cannot be combined. Request paths resolve from the working directory. Bindings do not rewrite destinations or define source sets; the TOML determines paths. Resolve symlinks in the checkout/config root before constructing `resourceRoot`, since the config root is canonicalized and containment uses path comparison. An empty request is accepted by the current decoder but integrations should send explicit nonempty bindings. ## Result On successful non-preview `pull --json`, stdout is: ``` {"protocolVersion":1,"files":[{"output":"android","path":"/workspace/android/app/build/generated/sentiary/values/strings.xml","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]} ``` `protocolVersion` is integer `1`; `files` is an array (empty when no files), each with output name, absolute destination path, and lowercase 64-character SHA-256 of the written bytes. This shape is also emitted without an integration request. Failures return nonzero and a diagnostic on stderr, not a result object. Offline age diagnostics remain on stderr. ## Preview `pull --dry-run` emits an array, **not** a protocol-versioned result, even when `--json` is also supplied: ``` [{"output":"android","project":"mobile","format":"android","layout":"per_language","languageIds":["en"],"includeEmptyTranslations":false,"path":"/workspace/android/app/build/generated/sentiary/values/strings.xml"}] ``` Each item has string `output`, project **alias** `project`, string `format`, `layout` (`per_language` or `combined`), string-array `languageIds`, boolean `includeEmptyTranslations`, and absolute string `path`. `sourceLanguageId` is emitted only when nonempty (combined catalogs). Aliases report resolved source IDs in `languageIds`, while the path reflects the target locale. Preview does not write destinations or publish an online completed set. Wildcards/aliases can cause language lookup requests. `--offline --dry-run` resolves from a covering completed set. Root validation applies to both previews and actual results, but a preview is not a check of ownership or future filesystem success. # Download and cache internals User-facing guarantees and recovery steps live in [downloads and offline use](). ## Concurrency and write guarantees A kernel-backed per-config lock at `.sentiary/.lock` covers fetching, cache publication, and destination writes. Concurrent pulls sharing the same manifest wait until the lock is available; cancellation stops the wait. A process exit releases the lock. The lock file may remain: its presence alone does not indicate an active lock. Do not delete a lock file while another pull may hold it. Different configs sharing destinations are not coordinated. Avoid overlapping ownership. The cache has a separate process lock around reads, publication, and cleanup, released during network requests. Online and offline previews do not acquire the ownership lock. All requested exports must succeed before any generated destination changes. Files are staged beside their destinations and renamed individually. The manifest is replaced after file writes and obsolete-file cleanup. This is not a multi-file filesystem transaction: an I/O failure can leave some files updated. ## Conditional requests and completed sets Online pulls authenticate and request backend `export/state` protocol 1, then send stored ETags via `If-None-Match`. A valid `304` reuses verified bytes. The response generation, data revision, and exporter version must match the observed project state. Each changing project is retried up to three times, then fails. A server/access/download error never silently selects offline data. A completed set is published only after all requested exports are verified and saved as immutable SHA-256 objects. Publication precedes destination validation/writes. A destination conflict can therefore leave a usable completed set. Snapshot consistency is per project; it is not a cross-project transaction or a translation lockfile. A committed intermediate state of a bulk edit may be captured. ## Cache location and identity Precedence: `--cache-dir`, then `SENTIARY_CACHE_DIR`, then `os.UserCacheDir()/sentiary/exports-v1`. Relative overrides are relative to the process working directory. | OS | Usual default base | | --- | --- | | macOS | `$HOME/Library/Caches/sentiary/exports-v1` | | Linux | `$XDG_CACHE_HOME/sentiary/exports-v1`, otherwise `$HOME/.cache/sentiary/exports-v1` | | Windows | `%LocalAppData%\sentiary\exports-v1` | The cache adds a hash of the normalized server URL, then stores `objects/` and `sets/` with a `cache.lock`. Identity includes project ID, format, layout, language selection, combined source language, and empty-translation policy. Keys, output names, checkout paths, and destination paths are excluded. Renaming outputs, changing keys, or moving checkouts can reuse exports. There is one latest completed set per exact export request selection. Replacement removes only objects unreferenced by remaining selections; cleanup is best effort. Sets do not expire automatically. Protect cache contents like generated translations.