Note on authorship: This analysis and the proposed fix were developed together with Claude (Anthropic) over several diagnostic iterations on a local duplicate site. I ran the reproduction steps, captured debug logs, and validated the resulting fix on my real environment. The reasoning, code-tracing, and patch proposals came from the Claude session. I'm sharing it as-is because the fix solved a data-destructive bug for me, and it may save you time even if you'd implement it differently.
Affected versions: Confirmed in 3.0.255, very likely present since ~3.0.246 (related to skeltern's report on the forum).
Affected fieldtypes: FieldtypeTextLanguage, FieldtypeTextareaLanguage, and likely all FieldtypeLanguageInterface implementations when used inside a Repeater (and presumably FieldsetPage / RepeaterMatrix, though those weren't directly tested).
Symptom
A TextLanguage field inside a Repeater loses its non-default-language values after re-saving the parent page — even when the user makes no changes to that field. The default language value is retained correctly.
Reproduction:
- Edit a page that has a repeater containing a multi-language text field
- Enter values for both default and a non-default language
- Save → values appear correctly on the front-end
- Re-open the same page in admin (no changes made)
- Save again → non-default language values are silently wiped from the DB
Root cause
The bug has two interacting layers — a prefill failure and a destructive save.
Layer 1 — prefill fails for repeater items in output-formatted state
LanguageSupport::hookFieldGetInputfield() (LanguageSupport.module, ~line 697) loads the field's per-language values onto the Inputfield via $page->get($field->name). For repeater items, this hook is invoked multiple times per render cycle. Debug logging shows it is called:
- Twice with
$page->of() === false → $value is a LanguagesPageFieldValue (all language values present, correct)
- Then four more times with
$page->of() === true → $value is a plain string (the formatted value in the current user's language)
In the output-formatted calls, the existing code falls into the else branch of:
foreach($languages as $language) {
$languageValue = '';
if($value instanceof LanguagesPageFieldValue) {
$languageValue = $value->getLanguageValue($language->id);
} else {
if($language->isDefault) $languageValue = $value;
}
$inputfield->set('value' . $language->id, $languageValue); // overwrites with ''
}
Because last-write-wins, the value{$languageID} settings populated by the earlier of=false calls get overwritten with empty strings. The rendered HTML for the non-default language inputs is then missing the value= attribute entirely — even though the DB still contains the data.
We were unable to determine in this debugging session which exact code path is responsible for the of=true calls on the RepeaterPage during the edit render. It may be FieldtypeRepeater::formatValue() being triggered by something upstream (a hook, an autoloaded module, or a getPageEditFields call). What matters is that the hook needs to handle this case defensively, since RepeaterPage state can be toggled by code outside ProcessPageEdit's control.
Layer 2 — destructive save when prefill is missing
LanguagesPageFieldValue::setFromInputfield() (LanguagesPageFieldValue.php, ~line 210) writes every language value back unconditionally:
foreach($this->wire()->languages as $language) {
$key = $language->isDefault() ? 'value' : 'value' . $language->id;
$this->setLanguageValue($language->id, $inputfield->$key);
}
There is no guard for the case where $inputfield->$key is null (key was never set on the Inputfield) or '' (rendered without value attribute → POST submits empty). Either way, the previously-stored DB value gets overwritten on the next save.
Reproduction trace from debug logging
For field_button_txt inside a repeater item (pageId 25270, fieldId 110, on a page with 6 languages where 4 were originally set to statusHidden):
ENTRY hookFieldGetInputfield: pageId=25270, of=false, value_class=LanguagesPageFieldValue, uk="DEBUGTEST"
ENTRY hookFieldGetInputfield: pageId=25270, of=false, value_class=LanguagesPageFieldValue, uk="DEBUGTEST"
ENTRY hookFieldGetInputfield: pageId=25270, of=true, value_class=string, uk=""
ENTRY hookFieldGetInputfield: pageId=25270, of=true, value_class=string, uk=""
ENTRY hookFieldGetInputfield: pageId=25270, of=true, value_class=string, uk=""
ENTRY hookFieldGetInputfield: pageId=25270, of=true, value_class=string, uk=""
After this render, the rendered HTML for the UK input is:
<input id="Inputfield_button_txt_repeater25270__22664"
name="button_txt_repeater25270__22664"
type="text" maxlength="2048" data-field-name="button_txt">
<!-- note: no value= attribute -->
On the subsequent save (no user changes), the POST submits an empty string for that input, and setFromInputfield writes '' to data[22664]. SleepValue then persists this to the DB as data22664=''.
Proposed fix
Two minimal, defensive changes resolve the issue:
1. wire/modules/LanguageSupport/LanguageSupport.module — hookFieldGetInputfield(), around line 697-718
// see if this fieldtype supports languages natively
if($field->type instanceof FieldtypeLanguageInterface && $useLanguages) {
$inputfield->set('useLanguages', true);
$value = $page->get($field->name);
// If $value is not a LanguagesPageFieldValue (e.g. because the page is in
// output-formatted state and the value got cast to a string for the current
// user language), temporarily disable output formatting and re-read the value
// so we get the full multi-language object.
if(!$value instanceof LanguagesPageFieldValue) {
$wasFormatted = $page->of();
if($wasFormatted) {
$page->of(false);
$value = $page->get($field->name);
$page->of($wasFormatted);
}
}
// Set per-language values, but only overwrite when we have a proper
// LanguagesPageFieldValue. For the default language a plain string is also
// acceptable. Otherwise leave existing per-language values intact rather than
// clobbering them with empty strings.
foreach($languages as $language) {
if($value instanceof LanguagesPageFieldValue) {
$languageValue = $value->getLanguageValue($language->id);
$inputfield->set('value' . $language->id, $languageValue);
} else if($language->isDefault) {
$inputfield->set('value' . $language->id, $value);
}
}
}
2. wire/modules/LanguageSupport/LanguagesPageFieldValue.php — setFromInputfield(), around line 210
public function setFromInputfield(Inputfield $inputfield) {
foreach($this->wire()->languages as $language) {
if($language->isDefault()) {
$key = 'value';
} else {
$key = 'value' . $language->id;
}
// Skip null values: when an Inputfield's per-language attribute was
// never populated, we must not interpret that as "set this language to
// empty" — that would wipe existing DB values.
$value = $inputfield->$key;
if($value === null) continue;
$this->setLanguageValue($language->id, $value);
}
}
Fix #1 addresses the root cause (the prefill needs to handle output-formatted RepeaterPages). Fix #2 is defense-in-depth against any other path where a per-language Inputfield attribute might be unset — without it, edge cases like custom Inputfield hooks could still cause data loss.
Verified fix behavior
Both fixes applied together on a local ProcessWire 3.0.255 install. Tested scenarios:
- The original bug: ✅ resolved. Non-default language values now prefill correctly in HTML and survive subsequent saves
- New repeater items: ✅ still work normally
- Editing/clearing non-default language values: ✅ saves correctly when the user explicitly empties an input
- Non-repeater multi-language fields: ✅ no regression observed
- Multiple repeater items on the same page: ✅ all retain their values
Related context
This issue may explain skeltern's forum report from March 2025 about a regression introduced in 3.0.246. The hook-typo on line 210 of LanguageSupport.module ('hookInputfieldGetLanguageValue' registered for both getLanguageValue and setLanguageValue method hooks) is unrelated to this specific bug but was noticed during analysis and may want a separate cleanup.
Environment
- ProcessWire 3.0.255
- 6 languages configured: NL (default), EN, SE, DK, NO, UK
- 4 of the non-default languages were initially set to
statusHidden; bug persists regardless of Hidden status
Affected versions: Confirmed in 3.0.255, very likely present since ~3.0.246 (related to skeltern's report on the forum).
Affected fieldtypes:
FieldtypeTextLanguage,FieldtypeTextareaLanguage, and likely allFieldtypeLanguageInterfaceimplementations when used inside a Repeater (and presumably FieldsetPage / RepeaterMatrix, though those weren't directly tested).Symptom
A TextLanguage field inside a Repeater loses its non-default-language values after re-saving the parent page — even when the user makes no changes to that field. The default language value is retained correctly.
Reproduction:
Root cause
The bug has two interacting layers — a prefill failure and a destructive save.
Layer 1 — prefill fails for repeater items in output-formatted state
LanguageSupport::hookFieldGetInputfield()(LanguageSupport.module, ~line 697) loads the field's per-language values onto the Inputfield via$page->get($field->name). For repeater items, this hook is invoked multiple times per render cycle. Debug logging shows it is called:$page->of() === false→$valueis aLanguagesPageFieldValue(all language values present, correct)$page->of() === true→$valueis a plain string (the formatted value in the current user's language)In the output-formatted calls, the existing code falls into the
elsebranch of:Because last-write-wins, the
value{$languageID}settings populated by the earlierof=falsecalls get overwritten with empty strings. The rendered HTML for the non-default language inputs is then missing thevalue=attribute entirely — even though the DB still contains the data.We were unable to determine in this debugging session which exact code path is responsible for the
of=truecalls on the RepeaterPage during the edit render. It may beFieldtypeRepeater::formatValue()being triggered by something upstream (a hook, an autoloaded module, or a getPageEditFields call). What matters is that the hook needs to handle this case defensively, since RepeaterPage state can be toggled by code outside ProcessPageEdit's control.Layer 2 — destructive save when prefill is missing
LanguagesPageFieldValue::setFromInputfield()(LanguagesPageFieldValue.php, ~line 210) writes every language value back unconditionally:There is no guard for the case where
$inputfield->$keyisnull(key was never set on the Inputfield) or''(rendered without value attribute → POST submits empty). Either way, the previously-stored DB value gets overwritten on the next save.Reproduction trace from debug logging
For
field_button_txtinside a repeater item (pageId 25270, fieldId 110, on a page with 6 languages where 4 were originally set tostatusHidden):After this render, the rendered HTML for the UK input is:
On the subsequent save (no user changes), the POST submits an empty string for that input, and
setFromInputfieldwrites''todata[22664]. SleepValue then persists this to the DB asdata22664=''.Proposed fix
Two minimal, defensive changes resolve the issue:
1.
wire/modules/LanguageSupport/LanguageSupport.module—hookFieldGetInputfield(), around line 697-7182.
wire/modules/LanguageSupport/LanguagesPageFieldValue.php—setFromInputfield(), around line 210Fix #1 addresses the root cause (the prefill needs to handle output-formatted RepeaterPages). Fix #2 is defense-in-depth against any other path where a per-language Inputfield attribute might be unset — without it, edge cases like custom Inputfield hooks could still cause data loss.
Verified fix behavior
Both fixes applied together on a local ProcessWire 3.0.255 install. Tested scenarios:
Related context
This issue may explain skeltern's forum report from March 2025 about a regression introduced in 3.0.246. The hook-typo on line 210 of LanguageSupport.module (
'hookInputfieldGetLanguageValue'registered for bothgetLanguageValueandsetLanguageValuemethod hooks) is unrelated to this specific bug but was noticed during analysis and may want a separate cleanup.Environment
statusHidden; bug persists regardless of Hidden status