johannschopplich / kirby-lingohub
Lingohub translation service integration for Kirby CMS
Package info
github.com/johannschopplich/kirby-lingohub
Type:kirby-plugin
pkg:composer/johannschopplich/kirby-lingohub
Fund package maintenance!
Requires
- php: >=8.2
- getkirby/composer-installer: ^1
- johannschopplich/kirby-tools-utils: ^0.1
Requires (Dev)
- getkirby/cms: ^5
- phpunit/phpunit: ^12
- vlucas/phpdotenv: ^5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Kirby Lingohub
Kirby Lingohub is a plugin for Kirby CMS that syncs content with the Lingohub translation service – blocks, layouts, structures, and objects included, even blocks nested in custom blocks.
Setup • Options • Field Options • Key Format
When to Use
| I want to… | Use |
|---|---|
| Upload and download translations from the Panel | lingohub view button |
| See each language's translation status | lingohub-status section |
| Change a field's translations only in Lingohub | translateExternalOnly |
| Translate a field only in Kirby | translateInKirbyOnly |
| Keep a field out of Lingohub | translate: false |
Features
☁️ Upload and Download
The lingohub view button opens a dropdown with Upload, Download Translations, and Open in Lingohub. Add it to the buttons of a page, Site, or file blueprint:
buttons: - preview - settings - lingohub - languages - status
Upload sends the languages you pick: the default language, preselected, and the translations that already exist in Kirby. Translations with no translated, draft, or approved segments in Lingohub yet are preselected too – handy when you move an existing site to Lingohub. Lingohub imports an upload in the background, and the status updates once it has, usually after a few seconds.
Download Translations fetches the segments of the chosen status – All, Translated and Approved, or Approved (the default) – and writes them into each selected language. Languages whose segments are all approved are preselected. A segment the download doesn't include keeps the language's current text, or the default language's if it has none. Only users who may update the page, Site, or file can download into it. A download that changes the title also needs the changeTitle permission.
Open in Lingohub opens the project in Lingohub's translation editor, with the default language as the source and the current language as the target.
Note
Kirby 4 has no custom view buttons. The plugin backports the lingohub button to page views and places it before the languages dropdown.
🚦 Translation Status
Next to the dropdown, a status button shows the current language's status in Lingohub: Approved once all its segments are approved, In Translation while some aren't, and Not Uploaded before the first upload. Click it for the segment counts per status, the failed quality checks, and the date of the last update. If the status can't be loaded, e.g. because Lingohub rejects the API key, the button reads Unavailable and a click shows the error.
The lingohub-status section lists the same counts for every language of the page, Site, or file that Lingohub holds, or the error if the status can't be loaded:
sections: lingohubStatus: type: lingohub-status label: Translation Status
🔒 Translate Only in Lingohub
Top-level fields with translateExternalOnly stay editable in the default language, but non-admins can't save a changed value in any other language. Use it for fields whose translations belong to Lingohub alone, so its translation memory stays consistent.
🏠 Translate Only in Kirby
Fields with translateInKirbyOnly never leave Kirby: uploads skip them, and downloads keep each language's own value – useful for images or files that differ per language.
Requirements
- Kirby 4 or Kirby 5
Installation
Composer (Recommended)
composer require johannschopplich/kirby-lingohub
Manual Installation
Download and copy this repository to /site/plugins/kirby-lingohub.
Setup
1. Kirby Languages
The plugin needs a multi-language site. Give every language a locale: the plugin derives the Lingohub language code from it, with _ replaced by - and a .UTF-8 suffix dropped. With an array locale, it reads the LC_ALL entry. A language without a locale falls back to its code.
# /site/languages/en.php return [ 'code' => 'en', 'default' => true, 'locale' => 'en_US', // Lingohub: `en-US` 'name' => 'English' ];
2. Lingohub Project
Create a project in your Lingohub workspace and choose File import. Lingohub won't continue without a first file, so upload a JSON file named after the default language's code, e.g. en-US.json with {"title": "Home"}. Then pick the project type JSON:
Lingohub detects the source language from the file name. Add the other Kirby languages as targets, with the language codes derived from the locales: en-US for en_US, de-DE for de_DE.
Then create an API key for the workspace with full permissions for Resources – uploads, downloads, and the status need nothing else.
3. Plugin Options
Copy the workspace and project IDs from the Lingohub URLs:
workspace/space_16kPs3bRIpXi-29323/dashboard→ workspace IDspace_16kPs3bRIpXi-29323project/pr_18JCETCbT9NW-31003/branches→ project IDpr_18JCETCbT9NW-31003
Add them and the API key to your config.php, see Options. Then add the lingohub button and, if you like, the lingohub-status section to your blueprints.
Options
All options are namespaced under johannschopplich.lingohub. Each is required, and the Panel shows an error while one is missing.
| Option | Default | Description |
|---|---|---|
apiKey |
– | Lingohub API key with full Resources permissions. |
workspaceId |
– | ID of the Lingohub workspace. |
projectId |
– | ID of the Lingohub project. |
Example config.php:
return [ 'languages' => true, 'johannschopplich.lingohub' => [ 'apiKey' => env('LINGOHUB_API_KEY'), 'workspaceId' => 'space_16kPs3bRIpXi-29323', 'projectId' => 'pr_18JCETCbT9NW-31003' ] ];
Field Options
Uploads include the page or Site title and every text-like field: list, tags, text, textarea, writer, markdown, and custom fields that extend one of them – also inside blocks, layouts, structures, and objects. Hidden blocks are skipped. Three blueprint options change what goes to Lingohub:
| Option | Upload | Download |
|---|---|---|
translate: false |
Skipped | Takes the default language's value, like Kirby |
translateInKirbyOnly: true |
Skipped | Keeps the language's own value |
translateExternalOnly: true |
Sent | Written; non-admins can't change it in the Panel |
fields: text: type: blocks translateExternalOnly: true image: type: files translateInKirbyOnly: true
translateExternalOnly covers the top-level fields of pages, the Site, and files. Admins and the default language are exempt.
Important
Set translateInKirbyOnly on fields, not on block fieldsets: Kirby drops unknown keys at a fieldset's root.
Key Format
Lingohub's JSON (Standard) format is a flat key–value map, and a segment keeps its translations only as long as its key stays the same. The plugin joins the names along a value's path with _:
| Field | Key |
|---|---|
| Text-like field | {field} |
| Structure | {field}_{rowIndex}_{subfield} |
| Object | {field}_{subfield} |
| Blocks | {field}_{blockId}_{blockType}_{subfield} |
| Layout | {field}_{blockId}_{blockType}_{subfield} |
Nested fields continue the chain, e.g. blocks inside a custom block: {field}_{blockId}_{blockType}_{subfield}_{nestedBlockId}_{nestedBlockType}_{nestedSubfield}. Layout and column IDs are not part of a key.
Each model is one Lingohub resource per language. Its path is the page or file ID (site for the Site), its file name {blueprint}_{locale}.json, e.g. notes/my-note/note_de-DE.json. The ID holds neither the draft status nor the sorting number, so publishing or sorting a page keeps its resource. Changing a page's slug, moving it, or renaming a file changes the path, and changing a model's template or a language's locale changes the file name, so Lingohub treats it as a new resource.
License
MIT License © 2025-PRESENT Johann Schopplich


