Somewhere in your application a heading reads menutab__content, or
a breadcrumb reads module__myapp_thing, or the administrator
restrictions screen lists raw keys where names should be. The page works. Nothing
is logged. It simply looks unfinished to whoever is paying you.
Invision Community renders a missing language key as the key itself. That is a sensible fallback and a poor advertisement, and there are two distinct ways to walk into it.
1. Keys you forgot to define
Every module in data/modules.json needs a
module__<app>_<module> string. They appear on the
administrator restrictions screen and in front-end breadcrumbs,
so a missing one is customer-visible, not just staff-visible.
{
"admin": { "settings": { ... } },
"front": { "podcast": { ... } }
}
/* needs BOTH */
'module__myapp_settings' => "My App",
'module__myapp_podcast' => "My App",
Audit it in one line rather than trusting your memory:
python3 -c "
import json
m=json.load(open('applications/myapp/data/modules.json'))
lang=open('applications/myapp/dev/lang.php').read()
for area,mods in m.items():
for mod in mods:
k='module__myapp_%s' % mod
print(k, 'present' if k in lang else 'MISSING')"
This was written up once here, then walked into twice more on later applications — including one that had already been published, where the restrictions screen used to decide which staff can touch what was listing internal keys. It is worth running on every app, not remembering.
2. Keys that were never yours to define
The second kind is subtler. You reference something that does not exist, and because the fallback is the key itself, it looks like a missing translation rather than a wrong value.
data/acpmenu.json puts your menu under a tab. There are exactly
six, and content is not one of them:
grep -oE "menutab__[a-z]+" applications/core/dev/lang.php | sort -u # menutab__community # menutab__core # menutab__customization # menutab__members # menutab__stats # menutab__support
Use community for anything member-facing, members for
member management, customization for appearance. Inventing
content because it describes your app best produces a sidebar heading
that reads menutab__content, and no error at all.
Related: addToStack returns a placeholder, not text
A third way to get a machine-readable string in front of a person, and the one
most likely to end up in stored data rather than on a page. addToStack()
returns a token for the language stack to substitute at render time. In a
task, a queue, a CLI script or a feed generator, nothing renders — so the token is
what gets saved:
/* wrong in a task: writes a 32-character hash as the topic title */ $topic->title = $lang->addToStack( 'my_title' ); /* right: returns the string, and throws if the key is missing */ $title = \IPS\Lang::load( \IPS\Lang::defaultLanguage() )->get( 'my_title' );
See addToStack() often returns a placeholder, not translated text for the full version. Between them these three account for most raw strings a customer ever sees.
The reliable way to catch all of it
None of these throw, none fail a lint, and none fail a test that checks a page returns 200. Look at the rendered screen. Capturing screenshots of your own AdminCP and front-end pages as a matter of routine — not only when a listing needs images — is how every one of these was actually found.
Related application: Discord Integration — Discord Integration names every module and menu properly, so the administrator restrictions screen reads in English rather than in internal keys.
Recommended Comments