Skip to main content

Introduction

Aikeedo’s interface is translated with GNU gettext. English source text lives in the code, and translations live in .po catalog files you can edit by hand or fill in automatically. Three console commands keep everything in step: Run them from your installation root:
Extract and translate both run the build step for you, so most of the time you only need the first two.

Where translations live

Where {organization} is usually heyaikeedo, {theme} is default for the standard theme, and {webroot} is public — or public_html on some installations, such as cPanel. Inside each of those, catalogs follow the gettext convention:
The /{webroot}/locale directory is generated output, not something you edit. It has to be writable for app:locale:build to work.

Translating

The usual workflow

After changing any wording in your templates:
1

Extract the strings

You will be asked which targets to scan. Choose Core application for the app and admin interface, Everything for that plus every plugin and theme on your installation, or name them individually — several at once, separated by commas.
2

Translate what is missing

You will be asked which languages to do — All languages, or a few by name — then shown how many strings and API requests that means before anything is sent.
3

Check the result

Open the app in the language you translated. The browser dictionaries are rebuilt automatically at the end of the previous step.

Translating by hand

You never have to use the AI translator. Open the .po file for your language, find the msgid entries and fill in the msgstr fields:
Always leave the msgid values unchanged. Only edit msgstr.
After editing a .po file by hand, rebuild the browser dictionaries:
No terminal? Settings → Clear cache in the admin area rebuilds them too.
This is the one step that is easy to forget. If your changes show up on server-rendered pages but not in toasts, buttons or other interface text produced by JavaScript, the dictionaries are stale — rebuild them.

The AI translator

app:locale:translate sends the untranslated entries to a language model and writes the answers back into the catalogs.
The catalog is saved after every group of requests, so stopping the command with Ctrl-C loses at most one group — running it again carries on from where it stopped.

Your API key

The translator needs an API key, and it is your key for this one task. It has nothing to do with the AI providers you configured in the admin area, and it is never saved anywhere. The key is read from, in order:
  1. The --api-key option
  2. The AIKEEDO_TRANSLATOR_API_KEY environment variable
  3. The OPENAI_API_KEY environment variable
  4. A hidden prompt, if none of the above are set
The key is used for the run and then forgotten. It is never written to .env, to your settings, or to the log — so you will be asked for it again next time unless you set an environment variable.

Using another provider

Anything that speaks the OpenAI chat completions API works, including OpenRouter, Groq, Together and a local Ollama server:

What the translator protects

Strings such as Hello :name and Version %s contain placeholders that are replaced at runtime. A translation may move a placeholder to wherever the target language needs it, but never renames or drops one.Every answer is checked, and any that gets this wrong is sent back once with an explanation. If it still comes back wrong, the entry is left untranslated and listed at the end of the run rather than being written out broken.
Many strings carry a context that says where they appear — button, heading, label, country and so on. This is what stops “Post” the button and “Post” the noun becoming the same word. The context is sent with each string so the model can pick the right wording.
Languages need different numbers of plural forms — one for Japanese, two for German, four for Russian, six for Arabic. The translator asks for exactly the number your language needs, explains which numbers each form covers, and rejects an answer with the wrong number of forms.

Managing available languages

Go to Settings → Languages in the admin area. The page lists every language that ships with Aikeedo, with:
  • Enable toggle — enabled languages appear in the language switcher and can be chosen by your users.
  • Default language — what a visitor sees before they pick one themselves. Only an enabled language can be the default.
  • Completion — how much of the app and admin interface is translated for each language.
Click Save changes to apply. The page won’t let you disable every language or make a disabled language the default.
Aikeedo ships configuration for 30 languages. Whether the translations themselves are filled in depends on the release — run app:locale:translate --lang=<code> to complete any that are not.

The locale.json catalogue

/locale/locale.json is the catalogue of languages the installation knows about. Your choices on the Languages page are stored in the database and layered on top of it, so you don’t need to edit this file to enable or disable a language. Its enabled flags and default are only used as a fallback, for example before the database exists during installation:
Updates merge the shipped locale.json into yours, so a language added in a new release shows up on the Languages page of existing installations.

Adding a language

1

Add it to locale.json

Add an entry with its code, label and text direction (ltr or rtl).
2

Create the catalog

3

Fill it in

4

Enable it

Turn it on in Settings → Languages.

Themes and plugins

All official plugins and themes are translated. Update each installed plugin and theme to receive its translations.
Themes and plugins keep their own catalogs next to their code. The extract command lists everything it finds on your installation and asks which to include:
A plugin’s catalog is locale/{language}/LC_MESSAGES/messages.po. A theme’s is locale/{language}/LC_MESSAGES/theme.po, because theme strings belong to their own theme domain.
Installing, updating or removing a plugin rebuilds the browser dictionaries on its own, so a plugin that ships its own translations works as soon as it is installed.
What ships inside extra/ is a built copy of a theme, and building or updating the theme replaces it — along with any catalog changes made there. If you are developing a theme from its own repository, point the commands at that directory instead:
Pass the repository root. The package inside it — the static/ directory in a repository made from the theme starter kit — is found on its own, and its catalogs are written to static/locale.
See the Theme Development guide for how theme packages are laid out.

Marking text for translation

If you edit templates, wrap any text a person will read so it can be extracted. The same functions are available in Twig, PHP and JavaScript, and they take the same arguments everywhere. The context in p__() is never shown to anyone — it only tells the translator where the string appears. Plugins use the plain functions (__(), p__() and so on). Their catalogs are merged into the application’s own dictionary when the plugin is loaded. Themes name their domain instead — d__('theme', …), dp__('theme', …), dn__('theme', …) or dnp__('theme', …). A plain __() in a theme template is looked up in the application’s dictionary, not the theme’s, so it is left out when the theme’s strings are extracted.

Placeholders

Two styles work, and both are passed as the argument after the string:

JavaScript

Only plain text can be extracted, so __(someVariable) is skipped. That is deliberate — it is how messages coming back from the server get translated. Such messages have no text to find anywhere in the JavaScript, so they are listed in resources/assets/js/base/server-messages.js instead. The browser only receives the strings your JavaScript actually asks for, not the whole catalog, so pages stay small.

User language selection

For signed-in users

1

Open Settings

Open the account menu at the bottom-left of the app and go to Settings → General.
2

Choose a language

Under Preferences, pick a language from the Language selector.

For website visitors

The default theme puts a language switcher in the website header, usually shown as a globe icon or the current language code in the top-right corner.
The exact position depends on your theme. Some themes place it in the footer or a side menu instead.

Best practices

  1. Consistency — keep terminology the same across all your translations.
  2. Extract before you translate — a translation run only fills in what the catalogs already contain, so extract after changing any wording.
  3. Review machine translations — the AI translator is a strong starting point, not a substitute for a native speaker on customer-facing copy.
  4. Test each language — longer translations can change how a layout fits, and right-to-left languages need a look of their own.
  5. Keep up with releases — run extract and translate after each update to catch newly added strings.

Troubleshooting

The browser dictionaries are out of date. The quickest fix needs no terminal: Settings → Clear cache in the admin area rebuilds them.From the command line:
If neither changes anything, /{webroot}/locale is most likely not writable — the rebuild is skipped rather than allowed to fail, and the reason is written to the application log.
Check that it is enabled in Settings → Languages, then clear the application cache.
The answer changed or dropped a placeholder, so it was rejected rather than written out broken. Run the command again to retry those entries, or fill them in by hand.
Run app:locale:extract first — translation only fills in entries that are already in the catalogs.
If you still have trouble, check that your .po files are correctly formatted, that locale.json is valid JSON, and clear both your browser cache and the application cache after making changes.

Need Help?

If you need assistance with Aikeedo:

Professional Support

Get expert help from our team with a paid support subscription

Troubleshooting Guide

Check common issues and solutions