Skip to content

Multi-language values in Repeater lost on subsequent save — analysis and proposed fix #2249

@Alfred-Bes

Description

@Alfred-Bes

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:

  1. Edit a page that has a repeater containing a multi-language text field
  2. Enter values for both default and a non-default language
  3. Save → values appear correctly on the front-end
  4. Re-open the same page in admin (no changes made)
  5. 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.modulehookFieldGetInputfield(), 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.phpsetFromInputfield(), 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions