Populate flat array from a nested array with dynamic keys from Laravel post model

97 Views Asked by At

I'd like to use DeepL to translate my Laravel Post model. The problem is, their API requires that we submit a flat array of translations. E.g. our payload would be -

{
    "target_lang": "ES",
    "text": [
        "The post title",
        "A quote goes here (translatable)",
        "<p>A content block goes here (translatable)</p>",
        "A quote goes here",
        "<p>A content block goes here (translatable)<\/p>"
    ]
}

However, my actual post uses a 'content' array to define blocks of content, like below:

[
  {
    "type": "quote",
    "data": {
      "name": "A quote name",
      "quote": "A quote goes here (translatable)"
    }
  },
  {
    "type": "content",
    "data": {
      "content": "<p>A content block goes here (translatable)<\/p>"
    }
  },
  {
    "type": "quote",
    "data": {
      "name": "A quote name (not translatable)",
      "quote": "A quote goes here"
    }
  },
  {
    "type": "content",
    "data": {
      "content": "<p>A content block goes here (translatable)<\/p>"
    }
  }
]

The data above is very flexible and changeable but always hinges on the block type. I can easily flatten the data down using something like below:

public function translatableAttributes(): array
{
    return [
        $this->title,
        ...collect($this->content)
            ->mapWithKeys(fn (array &$content) => match ($content['type']) {
                'quote' => $content['data']['quote'],
                'content' => $content['data']['content'],
            })->flatten()->toArray(),
    ];
}

But the big issue I'm having is pushing that back into the array. I just can't think of a way to do this while maintaining its flexibility.

I've thought of perhaps implementing promises and then resolving them for each key but again can't think how to implement this.

I could make separate API calls but again this is very inefficient when I can translate 50 strings at a time in one API call.

2

There are 2 best solutions below

1
Alexander Chetverin On BEST ANSWER

I would suggest getting by with a simple array_map()

public function translatableAttributes(): array
{
    return [
        $this->title,
        ...array_map(
            fn($content) => $content['data'][$content['type']],
            $this->content,
        ),
    ];
}

public function applyTranslations(array $translations): void
{
    $this->title = $translations[0];
    $this->content = array_map(
        static function ($content, $translation) {
            $content['data'][$content['type']] = $translation;
            return $content;
        },
        $this->content,
        array_slice($translations, 1),
    );
}

And more complex, but more flexible (if, for example, there are several fields for translation in one content; then we will need to use foreach in getTranslatableKeys())

public function translatableAttributes(): array
{
    return [
        $this->title,
        ...array_map(
            fn($key) => Arr::get($this->content, $key),
            $this->getTranslatableKeys(),
        ),
    ];
}

private function getTranslatableKeys(): array
{
    return array_map(
        fn($content, $index) => $index.'.data.'.$content['type'],
        $this->content,
        array_keys($this->content),
    );
}

public function applyTranslations(array $translations): void
{
    $this->title = $translations[0];
    foreach ($this->getTranslatableKeys() as $index => $key) {
        Arr::set($this->content, $key, $translations[$index + 1]);
    }
}
2
mickmackusa On

A body-less foreach loop with array destructuring syntax can push the desired values by dynamic keypath into the result array.

Code: (Demo) (or with a loop body)

$payload = [
    'target_lang' => 'ES',
    'text' => ['The post title']
];
foreach ($array as ['type' => $t, 'data' => [$t => $payload['text'][]]]);
var_export($payload);

Output:

array (
  'target_lang' => 'ES',
  'text' => 
  array (
    0 => 'The post title',
    1 => 'A quote goes here (translatable)',
    2 => '<p>A content block goes here (translatable)</p>',
    3 => 'A quote goes here',
    4 => '<p>A content block goes here (translatable)</p>',
  ),
)