Skip to content

Allow manual translating field attributes #4271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
73af16e
fix: creating translatable models with multiple languages
Nov 10, 2021
db0b94b
Update src/app/Models/Traits/SpatieTranslatable/HasTranslations.php
Nov 22, 2021
a6c297a
Merge branch 'v5' into pr/3932
pxpm Mar 16, 2022
b2523a8
fix array translatable attribute and add tests
pxpm Mar 16, 2022
eb9b6c8
Apply fixes from StyleCI
StyleCIBot Mar 16, 2022
849c0cc
refactor
pxpm Mar 16, 2022
2de67f4
Apply fixes from StyleCI
StyleCIBot Mar 16, 2022
876e68e
refactor for readability
pxpm Mar 17, 2022
9a5c493
remove dumps
pxpm Mar 17, 2022
50655fe
Merge branch 'v5' into pr/3932
pxpm Mar 17, 2022
eea1619
Apply fixes from StyleCI
StyleCIBot Mar 17, 2022
90d85c3
refactor function
pxpm Mar 17, 2022
a1d587c
Apply fixes from StyleCI
StyleCIBot Mar 17, 2022
c5eec22
Merge branch 'v5' into pr/3932
pxpm Apr 4, 2022
5e2b708
Merge branch 'pr/3932' of https://github.com/Laravel-Backpack/CRUD in…
pxpm Apr 4, 2022
19bd256
Update src/app/Models/Traits/SpatieTranslatable/HasTranslations.php
pxpm Apr 4, 2022
4ffb47f
Update src/app/Models/Traits/SpatieTranslatable/HasTranslations.php
pxpm Apr 4, 2022
3dfb265
Apply fixes from StyleCI
StyleCIBot Apr 4, 2022
f87427a
Merge branch 'pr/3932' of https://github.com/Laravel-Backpack/CRUD in…
pxpm Apr 4, 2022
bfc9242
change method signatures, fix doc-blocks
pxpm Apr 4, 2022
9c13cc0
Apply fixes from StyleCI
StyleCIBot Apr 4, 2022
b805c85
Merge branch 'main' into pr/3932
pxpm Sep 26, 2022
7c52c64
Merge branch 'main' into pr/3932
pxpm Oct 31, 2022
d76a050
remove eloquent overrides, refactor fake columns
pxpm Nov 1, 2022
255a674
Apply fixes from StyleCI
StyleCIBot Nov 1, 2022
e3ef7f3
remove unused code
pxpm Nov 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions src/app/Models/Traits/HasFakeFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ trait HasFakeFields
*
* @param array $columns - the database columns that contain the JSONs
*/
public function addFakes($columns = ['extras'])
public function addFakes(array $columns = [])
{
$columns = ! empty($columns) ? $columns : $this->getFakeColumns();

foreach ($columns as $key => $column) {
if (! isset($this->attributes[$column])) {
continue;
Expand All @@ -38,21 +40,25 @@ public function addFakes($columns = ['extras'])
}
}

/**
* Return the model fake columns.
*
* @return array
*/
public function getFakeColumns()
{
return $this->fakeColumns ?? ['extras'];
}

/**
* Return the entity with fake fields as attributes.
*
* @param array $columns - the database columns that contain the JSONs
* @return Model
*/
public function withFakes($columns = [])
public function withFakes(array $columns = [])
{
$model = '\\'.get_class($this);

$columnCount = ((is_array($columns) || $columns instanceof Countable) ? count($columns) : 0);

if ($columnCount == 0) {
$columns = (property_exists($model, 'fakeColumns')) ? $this->fakeColumns : ['extras'];
}
$columns = ! empty($columns) ? $columns : $this->getFakeColumns();

$this->addFakes($columns);

Expand All @@ -62,22 +68,33 @@ public function withFakes($columns = [])
/**
* Determine if this fake column should be json_decoded.
*
* @param $column string fake column name
* @param string $column fake column name
* @return bool
*/
public function shouldDecodeFake($column)
public function shouldDecodeFake(string $column)
{
return ! in_array($column, array_keys($this->casts));
}

/**
* Determine if this fake column should get json_encoded or not.
*
* @param $column string fake column name
* @param string $column fake column name
* @return bool
*/
public function shouldEncodeFake($column)
public function shouldEncodeFake(string $column)
{
return ! in_array($column, array_keys($this->casts));
}

/**
* Check if the given column name is a fakeColumn.
*
* @param string $column
* @return bool
*/
public function isFakeColumn(string $column)
{
return in_array($column, $this->getFakeColumns());
}
}
93 changes: 30 additions & 63 deletions src/app/Models/Traits/SpatieTranslatable/HasTranslations.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Backpack\CRUD\app\Models\Traits\SpatieTranslatable;

use Illuminate\Support\Arr;
use Spatie\Translatable\HasTranslations as OriginalHasTranslations;

trait HasTranslations
Expand Down Expand Up @@ -32,7 +31,7 @@ public function getAttributeValue($key)
return parent::getAttributeValue($key);
}

$translation = $this->getTranslation($key, $this->locale ?: config('app.locale'));
$translation = $this->getTranslation($key, $this->locale ?: app()->getLocale());

// if it's a fake field, json_encode it
if (is_array($translation)) {
Expand All @@ -42,81 +41,49 @@ public function getAttributeValue($key)
return $translation;
}

public function getTranslation(string $key, string $locale, bool $useFallbackLocale = true)
public function setAttribute($key, $value)
{
$locale = $this->normalizeLocale($key, $locale, $useFallbackLocale);
if ($this->isTranslatableAttribute($key) && is_array($value)) {
// if it is a fake column, set the whole column translation
if ($this->isFakeColumn($key)) {
return $this->setTranslation($key, $this->getLocale(), $value);
}

$translations = $this->getTranslations($key);
// if none of the array keys match an available translation, translate the whole array
$possibleTranslations = array_keys($value);
$translatableLocales = array_keys($this->getAvailableLocales());

$translation = $translations[$locale] ?? '';
if (! array_intersect($translatableLocales, $possibleTranslations)) {
return $this->setTranslation($key, $this->getLocale(), $value);
}

if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $translation);
// otherwise assume developer provided the attribute translations. eg: ['name' => ['en' => 'name' => 'pt' => 'nome']]
return $this->setTranslations($key, $value);
}

return $translation;
}

/*
|--------------------------------------------------------------------------
| ELOQUENT OVERWRITES
|--------------------------------------------------------------------------
*/

/**
* Create translated items as json.
*
* @param array $attributes
* @return static
*/
public static function create(array $attributes = [])
{
$locale = $attributes['locale'] ?? \App::getLocale();
$attributes = Arr::except($attributes, ['locale']);
$non_translatable = [];

$model = new static();

// do the actual saving
foreach ($attributes as $attribute => $value) {
if ($model->isTranslatableAttribute($attribute)) { // the attribute is translatable
$model->setTranslation($attribute, $locale, $value);
} else { // the attribute is NOT translatable
$non_translatable[$attribute] = $value;
}
// Pass arrays and untranslatable attributes to the parent method.
if (! $this->isTranslatableAttribute($key) || is_array($value)) {
return parent::setAttribute($key, $value);
}
$model->fill($non_translatable)->save();

return $model;
// If the attribute is translatable and not already translated, set a
// translation for the current app locale.
return $this->setTranslation($key, $this->getLocale(), $value);
}

/**
* Update translated items as json.
*
* @param array $attributes
* @param array $options
* @return bool
*/
public function update(array $attributes = [], array $options = [])
public function getTranslation(string $key, string $locale, bool $useFallbackLocale = true)
{
if (! $this->exists) {
return false;
}
$locale = $this->normalizeLocale($key, $locale, $useFallbackLocale);

$locale = $attributes['_locale'] ?? \App::getLocale();
$attributes = Arr::except($attributes, ['_locale']);
$non_translatable = [];
$translations = $this->getTranslations($key);

// do the actual saving
foreach ($attributes as $attribute => $value) {
if ($this->isTranslatableAttribute($attribute)) { // the attribute is translatable
$this->setTranslation($attribute, $locale, $value);
} else { // the attribute is NOT translatable
$non_translatable[$attribute] = $value;
}
$translation = $translations[$locale] ?? '';

if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $translation);
}

return $this->fill($non_translatable)->save($options);
return $translation;
}

/*
Expand Down Expand Up @@ -206,7 +173,7 @@ public function __call($method, $parameters)
return parent::__call($method, $parameters);
break;

// do not translate any other methods
// do not translate any other methods
default:
return parent::__call($method, $parameters);
break;
Expand Down
100 changes: 100 additions & 0 deletions tests/Unit/CrudPanel/CrudPanelTranslatableTests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

namespace Backpack\CRUD\Tests\Unit\CrudPanel;

use Backpack\CRUD\Tests\Unit\Models\TranslatableModel;

class CrudPanelTrannslatableTest extends BaseDBCrudPanelTest
{
private $translatableFields = [
[
'name' => 'translatable_field',
],
[
'name' => 'fake_1',
'fake' => true,
'store_in' => 'translatable_fake',
],
[
'name' => 'fake_2',
'fake' => true,
'store_in' => 'translatable_fake',
],
[
'name' => 'translatable_field_with_mutator',
],
];

private $input_en = [
'translatable_field' => 'field en',
'fake_1' => 'fake en',
'fake_2' => 'fake2 en',
'translatable_field_with_mutator' => 'field mutator en',
];

private $input_fr = [
'translatable_field' => 'field fr',
'fake_1' => 'fake fr',
'fake_2' => 'fake2 fr',
'translatable_field_with_mutator' => 'field mutator fr',
];

public function testManuallyCreateModelTranslations()
{
config()->set('backpack.crud.locales', ['en' => 'English', 'fr' => 'French']);

$model = TranslatableModel::create([
'translatable_field' => ['en' => 'test en', 'fr' => 'test fr'],
'translatable_fake' => 'test',
'translatable_field_with_mutator' => 'test',
]);

$this->assertEquals('test en', $model->translatable_field);
$model->setLocale('fr');
$this->assertEquals('test fr', $model->translatable_field);
}

public function testCreateAndUpdateTranslatableFields()
{
config()->set('backpack.crud.locales', ['en' => 'English', 'fr' => 'French']);

$this->crudPanel->setModel(TranslatableModel::class);
$this->crudPanel->addFields($this->translatableFields);

$model = $this->crudPanel->create($this->input_en);
$model = $model->withFakes();

$this->assertEquals('field en', $model->translatable_field);
$this->assertEquals('fake en', $model->fake_1);
$this->assertEquals('fake2 en', $model->fake_2);
$this->assertEquals('FIELD MUTATOR EN', $model->translatable_field_with_mutator);

app()->setLocale('fr');

$model = $this->crudPanel->update($model->id, $this->input_fr);
$model->withFakes();

$this->assertEquals('field fr', $model->translatable_field);
$this->assertEquals('fake fr', $model->fake_1);
$this->assertEquals('fake2 fr', $model->fake_2);
$this->assertEquals('FIELD MUTATOR FR', $model->translatable_field_with_mutator);

$model->setLocale('en');
$this->assertEquals('field en', $model->translatable_field);
}

public function testPassArrayIntoTranslatableWithoutExplicitTranslation()
{
config()->set('backpack.crud.locales', ['en' => 'English', 'fr' => 'French']);

$model = TranslatableModel::create([
'translatable_field' => ['some_key' => 'some_value'],
'translatable_fake' => ['some_key' => 'some_value'],
'translatable_field_with_mutator' => ['some_key' => 'some_value'],
]);

$this->assertEquals(['some_key' => 'some_value'], json_decode($model->translatable_field, true));
$this->assertEquals(['some_key' => 'some_value'], json_decode($model->translatable_fake, true));
$this->assertEquals(['some_key' => 'SOME_VALUE'], json_decode($model->translatable_field_with_mutator, true));
}
}
30 changes: 30 additions & 0 deletions tests/Unit/Models/TranslatableModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Backpack\CRUD\Tests\Unit\Models;

use Backpack\CRUD\app\Models\Traits\CrudTrait;
use Backpack\CRUD\app\Models\Traits\SpatieTranslatable\HasTranslations;
use Illuminate\Database\Eloquent\Model;

class TranslatableModel extends Model
{
use CrudTrait, HasTranslations;

protected $table = 'translatables';
protected $fillable = ['translatable_field', 'translatable_fake', 'translatable_field_with_mutator'];

protected $fakeColumns = ['translatable_fake'];

protected $translatable = ['translatable_field', 'translatable_fake', 'translatable_field_with_mutator'];

public $timestamps = false;

public function setTranslatableFieldWithMutatorAttribute($value)
{
$this->attributes['translatable_field_with_mutator'] = ! is_array($value) ?
strtoupper($value) :
array_map(function ($item) {
return strtoupper($item);
}, (array) $value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTranslatablesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('translatables', function (Blueprint $table) {
$table->increments('id');
$table->text('translatable_field')->nullable();
$table->text('translatable_fake')->nullable();
$table->text('translatable_field_with_mutator')->nullable();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('translatables');
}
}