Repository navigation
Defects in English language files #5366
Description
ExpressionEngine 7.5.26 - Defects in English language files
Reported by: Slovak translation maintainer
Version audited: ExpressionEngine 7.5.26 (unmodified) + Forum 5.1.3, IP to Nation 3.0.0, Simple Commerce 2.2.1
Verified on: PHP 8.1.6
Scope: 74 English *_lang.php files (9,831 keys) cross-referenced against 2,708 PHP/HTML source files.
How this was found
While maintaining the Slovak translation I compared every English language string against its actual consumption in the codebase. Each key containing a placeholder was traced to its lang() / ee()->lang->line() call sites and classified by the substitution mechanism used there.
This surfaced a class of defects that a translator hits immediately but that is invisible when reading the language files alone: ExpressionEngine uses two unrelated placeholder conventions, and a handful of strings use the wrong one for their call site.
| Convention | Mechanism | Example call site |
|---|---|---|
%s, %d |
sprintf() |
sprintf(lang('key'), $val) |
%x, %y, %n, %time%, %max%, %overflow_names% |
str_replace() |
str_replace('%x', $val, lang('key')) |
For the record: %x is legitimate in EE and must not be "corrected" to %s. Of 34 %x occurrences, 26 are correctly consumed by str_replace, 7 are in dead keys, and exactly 1 is a genuine bug (§2.1).
All findings below were reproduced by executing the relevant sprintf calls with representative arguments.
1. Critical - fatal errors on PHP 8
1.1 version_update_inst - ValueError, breaks Settings › General
String: system/ee/language/english/expressionengine_info_lang.php:27
'version_update_inst' => '<a class="update" href="%d" title="Download Here" rel="external">Download the latest version</a> and follow the <a class="update" href="%i" title="Update Instructions" rel="external">Update Instructions</a>.',Call site: system/ee/ExpressionEngine/Controller/Settings/General.php:336
$banner->addToBody(sprintf(lang('version_update_inst'), $upgrade_url, $instruct_url));Two defects:
%iis not a valid PHP format specifier. On PHP 8sprintf()throws:ValueError: Unknown format specifier "i"%dcasts its argument to integer. A URL string becomes0, producinghref="0".
Reproduction:
sprintf(
'<a href="%d">Download</a> and <a href="%i">Instructions</a>',
'https://expressionengine.com/download',
'https://docs.expressionengine.com/update'
);
// PHP 8.1.6 → ValueError: Unknown format specifier "i"Impact: Any user visiting Settings › General while an update is available gets a fatal error instead of the update banner. On PHP 7 the page rendered but with a broken download link (href="0").
Suggested fix: use %s for both placeholders.
1.2 channel_form_lang.php - four validation strings expect 2 arguments, receive 1
Strings: system/ee/language/english/channel_form_lang.php
| Line | Key | Value |
|---|---|---|
| 24 | exact_length |
The %s field must be exactly %s characters in length. |
| 38 | matches |
The %s field does not match the %s field. |
| 40 | max_length |
The %s field cannot exceed %s characters in length. |
| 42 | min_length |
The %s field must be at least %s characters in length. |
Call sites: system/ee/legacy/libraries/Form_validation.php
// line 815 - one argument (field label)
$message = sprintf($line, $this->_translate_fieldname($row['label']));
// line 951 - one argument (rule parameter)
$message = sprintf($line, $param);Form_validation never passes more than one argument. On PHP 8:
ValueError: The arguments array must contain 2 items, 1 given
Compounding issue - key collision. The same four keys also exist in system/ee/language/english/form_validation_lang.php (lines 19, 47, 49, 51) with the correct single-placeholder form:
'max_length' => 'This field cannot exceed %s characters in length.',Whichever file is loaded last wins, so the failure is load-order dependent and therefore intermittent across contexts. Form_validation.php:1146 explicitly loads form_validation, which usually masks the problem - but any code path that loads channel_form afterwards will crash.
Suggested fix: drop the leading The %s field from the channel_form_lang.php variants, or remove the duplicated keys entirely and rely on form_validation_lang.php.
2. Incorrect output
2.1 sig_too_big - number rendered as hexadecimal
String: system/ee/language/english/member_lang.php:545
'sig_too_big' => 'Signatures can only contain %x characters',Call site: system/ee/ExpressionEngine/Addons/member/mod.member_images.php:107
sprintf(lang('sig_too_big'), $maxlength)This is the one place where %x reaches sprintf instead of str_replace. %x is lowercase hexadecimal, so:
sprintf('Signatures can only contain %x characters', 10000);
// → "Signatures can only contain 2710 characters"Impact: users are told a wrong, much smaller-looking limit. With the default sig_maxlength of 10000 the message reads "2710 characters".
Suggested fix: change %x to %s in the string (the call site already uses sprintf consistently with the rest of the file).
2.2 less_than - key collision corrupts relative dates
Defined twice with unrelated meanings:
| File | Line | Value |
|---|---|---|
system/ee/language/english/core_lang.php |
237 | less than |
system/ee/language/english/form_validation_lang.php |
41 | This field must be less than: %s |
Consumer: system/ee/legacy/libraries/Relative_date.php:43
$this->less_than = lang('less_than');Relative_date wants the core_lang value to build strings like "less than a minute ago". If form_validation_lang.php has been loaded, the output becomes:
This field must be less than: %s a minute ago
Suggested fix: rename one of them (e.g. validation_less_than).
2.3 running_current - %v is never substituted
String: system/ee/language/english/expressionengine_info_lang.php:19
'running_current' => 'ExpressionEngine is up to date (%v)',Call site: system/ee/ExpressionEngine/Controller/Settings/General.php:347
->withTitle(lang('running_current'))No sprintf, no str_replace. The literal %v is shown to the user. The very next line handles the sibling string correctly:
->addToBody(sprintf(lang('running_current_desc'), APP_VER))Suggested fix: either wrap the title in sprintf(..., APP_VER) and change %v to %s, or drop the placeholder from the string.
3. Missing language keys
| Key | Called from | Notes |
|---|---|---|
search_reindex_completed |
system/ee/ExpressionEngine/Service/Channel/Reindex.php:195 |
Typo in the code. search_reindexed_completed exists at utilities_lang.php:553 (Content reindexing complete. %s entries were reindexed.) and already carries the %s this call passes. |
removed_role |
system/ee/ExpressionEngine/Controller/Members/Roles/Roles.php:2006 |
Not defined anywhere. sprintf(lang('removed_role'), '<b>'.$role_name.'</b>') writes the literal string removed_role into the CP action log. |
4. Values passed by code but discarded by the string
The call site supplies data the English string has no placeholder for, so the information is silently dropped.
| Key | Language file | Call site | Discarded value |
|---|---|---|---|
file_saved |
admin_lang.php:389 |
Controller/Utilities/Translate.php:479 |
destination path |
rows_replaced |
admin_lang.php:753, tools_data_lang.php |
Controller/Utilities/Sandr.php:63 |
number of replacements |
imglib_unsafe_config |
image_lib_lang.php:25 |
legacy/libraries/Image_lib.php:685 |
offending path |
forum_board_removed_desc |
forum/…/forum_cp_lang.php:383 |
user/addons/forum/mcp.forum.php:1223 |
forum name |
access_privilege_caution |
members_lang.php:482 |
Controller/Members/Profile/Roles.php:87 |
icon markup (already hardcoded in the string) |
rel_ft_limit |
fieldtypes_lang.php:66 |
Addons/relationship/ft.relationship.php:709 |
second argument (string has one %s, two are passed) |
Worth noting that utilities_lang.php already has the correct variants - file_saved at line 119 (The translation file has been saved to <b>%s</b>) and rows_replaced at line 205 (… occurred: %s). The duplicates in admin_lang.php and tools_data_lang.php are the stale ones.
5. Dead keys containing placeholders
Not referenced anywhere in the codebase. Listed because they mislead translators into preserving placeholders in strings that will never render.
| Key | File | Placeholder |
|---|---|---|
unreadable_language |
installer_lang.php:97 |
%x |
click_to_recount |
admin_lang.php |
%x, %y |
system_path |
admin_lang.php |
%x |
auto_redirection |
core_lang.php |
%x |
page_rendered |
cp_lang.php |
%x |
queries_executed |
cp_lang.php |
%x |
if_replacing_templates |
tools_data_lang.php |
%x |
entry_title_with_title |
content_lang.php |
%title |
6. Duplicate keys across files
431 keys are defined in more than one English language file with differing values. Resolution depends on load order, which makes the effective string context-dependent.
27 of these are risky - one variant carries a placeholder and another does not, so depending on load order either a value is dropped or sprintf raises ArgumentCountError / ValueError on PHP 8.
Highest-impact clusters:
channel_form_lang.php×form_validation_lang.php- 13 validation keys (required,numeric,integer,alpha,alpha_dash,alpha_numeric,is_natural,is_natural_no_zero,is_numeric,valid_email,exact_length,max_length,min_length,matches)admin_lang.php×utilities_lang.php×tools_data_lang.php-file_saved,rows_replaced,total_resultsaddons_lang.php×rte_lang.php-settings_saved_desc
7. Spelling and markup errors in English strings
| File | Key | Current | Should be |
|---|---|---|---|
installer_lang.php:100 |
unreadable_dbdriver |
sytem/ee/installer/schema/ |
system/… |
installer_lang.php:103 |
update_step_error |
occured |
occurred |
admin_lang.php:145 |
analize |
Analize Tables - the key itself is misspelled too |
analyze / Analyze Tables |
admin_lang.php:551 |
not_writable_path |
writeable |
writable |
cp_lang.php:432 |
not_writable_path |
writeable |
writable |
members_lang.php:346 |
not_writable_path |
writeable |
writable |
tools_lang.php:75 |
trans_file_not_writable |
writeable |
writable |
utilities_lang.php:129 |
trans_file_not_writable |
writeable |
writable |
members_lang.php:438 |
your_password_anonymize_members_desc |
anomymize |
anonymize |
upload_lang.php:15 |
upload_invalid_dimensions |
exceedes |
exceeds |
core_lang.php:166 |
php72_intl_error |
extentsion |
extension |
cp_lang.php:895 |
text_icon |
Text file iconn |
icon |
sites_lang.php:31 |
invalid_short_name |
underscroes |
underscores |
cli_lang.php:257 |
command_make_fieldtype_building_fieldtype |
Building fieldype. |
fieldtype |
cli_lang.php:493 |
command_sync_conditional_fields_summary |
connditional |
conditional |
ip_to_nation_lang.php:35 |
ip_db_failed |
IP databse update failed |
database |
Unbalanced HTML - a stray </em> with no opening tag, which leaks into the rendered page:
system/user/addons/forum/language/english/forum_cp_lang.php:247-reply_notification_desc… when a <b>new</b> reply is made.</em><em>Separate multiple e-mails …system/user/addons/ip_to_nation/language/english/ip_to_nation_lang.php:63-update_info… rel="external">maxmind.com</a>.</em><em><i>The new data will …
Summary by severity
| Severity | Count | Items |
|---|---|---|
| Critical (fatal on PHP 8) | 2 | §1.1 version_update_inst, §1.2 channel_form_lang.php ×4 keys |
| High (wrong output shown to users) | 3 | §2.1 sig_too_big, §2.2 less_than, §2.3 running_current |
| Medium (missing keys / lost data) | 8 | §3 ×2, §4 ×6 |
| Low (dead keys, duplicates, spelling) | 24+ | §5 ×8, §6, §7 ×16 |
Notes for other translators
Three things that are easy to get wrong when translating EE and are worth documenting upstream:
%x,%y,%nand%name%tokens are notsprintfplaceholders. They must be preserved verbatim; substituting%sbreaks them. Only%sand%dgo throughsprintf.- Validation strings should carry exactly one
%s.Form_validationalways passes a single argument. - Where the English string is missing a placeholder that the code supplies (§4), a translation may legitimately add
%s- the Slovak files already do this forfile_savedandrows_replaced, matchingutilities_lang.phprather than the staleadmin_lang.phpcopies.
Activity