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
2
Translate what is missing
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..po file by hand, rebuild the browser dictionaries:
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:- The
--api-keyoption - The
AIKEEDO_TRANSLATOR_API_KEYenvironment variable - The
OPENAI_API_KEYenvironment variable - A hidden prompt, if none of the above are set
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
Placeholders stay intact
Placeholders stay intact
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.Context is passed along
Context is passed along
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.Plural forms are complete
Plural forms are complete
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.
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:
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.
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 Pass the repository root. The package inside it — the
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:static/ directory in
a repository made from the theme starter kit — is found on its own, and its
catalogs are written to static/locale.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
- Consistency — keep terminology the same across all your translations.
- Extract before you translate — a translation run only fills in what the catalogs already contain, so extract after changing any wording.
- Review machine translations — the AI translator is a strong starting point, not a substitute for a native speaker on customer-facing copy.
- Test each language — longer translations can change how a layout fits, and right-to-left languages need a look of their own.
- Keep up with releases — run extract and translate after each update to catch newly added strings.
Troubleshooting
Interface text produced by JavaScript is still English
Interface text produced by JavaScript is still English
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.A language does not appear in the switcher
A language does not appear in the switcher
Check that it is enabled in Settings → Languages, then clear the
application cache.
The translator says a string was left untranslated
The translator says a string was left untranslated
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.
New wording is missing from the catalogs
New wording is missing from the catalogs
Run
app:locale:extract first — translation only fills in entries that are
already in the catalogs..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