Skip to content

Commit

Permalink
Merge pull request #4267 from Laravel-Backpack/translatable-with-fall…
Browse files Browse the repository at this point in the history
…backs

Translatable with fallbacks
  • Loading branch information
pxpm authored Jun 19, 2024
2 parents 043cd5c + c378837 commit 761733f
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 78 deletions.
36 changes: 26 additions & 10 deletions src/app/Library/CrudPanel/CrudPanel.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@ public function getRequest()
*
* @param string $model_namespace Full model namespace. Ex: App\Models\Article
*
* @throws \Exception in case the model does not exist
* @throws Exception in case the model does not exist
*/
public function setModel($model_namespace)
{
if (! class_exists($model_namespace)) {
throw new \Exception('The model does not exist.', 500);
throw new Exception('The model does not exist.', 500);
}

if (! method_exists($model_namespace, 'hasCrudTrait')) {
throw new \Exception('Please use CrudTrait on the model.', 500);
throw new Exception('Please use CrudTrait on the model.', 500);
}

$this->model = new $model_namespace();
Expand Down Expand Up @@ -198,7 +198,7 @@ public function setRoute($route)
* @param string $route Route name.
* @param array $parameters Parameters.
*
* @throws \Exception
* @throws Exception
*/
public function setRouteName($route, $parameters = [])
{
Expand All @@ -207,7 +207,7 @@ public function setRouteName($route, $parameters = [])
$complete_route = $route.'.index';

if (! \Route::has($complete_route)) {
throw new \Exception('There are no routes for this route name.', 404);
throw new Exception('There are no routes for this route name.', 404);
}

$this->route = route($complete_route, $parameters);
Expand Down Expand Up @@ -348,7 +348,7 @@ public function sync($type, $fields, $attributes)
* @param int $length Optionally specify the number of relations to omit from the start of the relation string. If
* the provided length is negative, then that many relations will be omitted from the end of the relation
* string.
* @param \Illuminate\Database\Eloquent\Model $model Optionally specify a different model than the one in the crud object.
* @param Model $model Optionally specify a different model than the one in the crud object.
* @return string Relation model name.
*/
public function getRelationModel($relationString, $length = null, $model = null)
Expand Down Expand Up @@ -380,7 +380,7 @@ public function getRelationModel($relationString, $length = null, $model = null)
* Get the given attribute from a model or models resulting from the specified relation string (eg: the list of streets from
* the many addresses of the company of a given user).
*
* @param \Illuminate\Database\Eloquent\Model $model Model (eg: user).
* @param Model $model Model (eg: user).
* @param string $relationString Model relation. Can be a string representing the name of a relation method in the given
* Model or one from a different Model through multiple relations. A dot notation can be used to specify
* multiple relations (eg: user.company.address).
Expand All @@ -392,6 +392,7 @@ public function getRelatedEntriesAttributes($model, $relationString, $attribute)
$endModels = $this->getRelatedEntries($model, $relationString);
$attributes = [];
foreach ($endModels as $model => $entries) {
/** @var Model $model_instance */
$model_instance = new $model();
$modelKey = $model_instance->getKeyName();

Expand Down Expand Up @@ -430,7 +431,7 @@ public function getRelatedEntriesAttributes($model, $relationString, $attribute)
/**
* Parse translatable attributes from a model or models resulting from the specified relation string.
*
* @param \Illuminate\Database\Eloquent\Model $model Model (eg: user).
* @param Model $model Model (eg: user).
* @param string $attribute The attribute from the relation model (eg: the street attribute from the address model).
* @param string $value Attribute value translatable or not
* @return string A string containing the translated attributed based on app()->getLocale()
Expand All @@ -446,7 +447,7 @@ public function parseTranslatableAttributes($model, $attribute, $value)
}

if (! is_array($value)) {
$decodedAttribute = json_decode($value, true);
$decodedAttribute = json_decode($value, true) ?? ($value !== null ? [$value] : []);
} else {
$decodedAttribute = $value;
}
Expand All @@ -462,11 +463,26 @@ public function parseTranslatableAttributes($model, $attribute, $value)
return $value;
}

public function setLocaleOnModel(Model $model)
{
$useFallbackLocale = $this->shouldUseFallbackLocale();

if (method_exists($model, 'translationEnabled') && $model->translationEnabled()) {
$locale = $this->getRequest()->input('_locale', app()->getLocale());
if (in_array($locale, array_keys($model->getAvailableLocales()))) {
$model->setLocale(! is_bool($useFallbackLocale) ? $useFallbackLocale : $locale);
$model->useFallbackLocale = (bool) $useFallbackLocale;
}
}

return $model;
}

/**
* Traverse the tree of relations for the given model, defined by the given relation string, and return the ending
* associated model instance or instances.
*
* @param \Illuminate\Database\Eloquent\Model $model The CRUD model.
* @param Model $model The CRUD model.
* @param string $relationString Relation string. A dot notation can be used to chain multiple relations.
* @return array An array of the associated model instances defined by the relation string.
*/
Expand Down
32 changes: 22 additions & 10 deletions src/app/Library/CrudPanel/Traits/Read.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Backpack\CRUD\app\Exceptions\BackpackProRequiredException;
use Exception;
use Illuminate\Support\Facades\Route;

/**
* Properties and methods used by the List operation.
Expand All @@ -21,7 +22,7 @@ public function getCurrentEntryId()
return $this->entry->getKey();
}

$params = \Route::current()->parameters();
$params = Route::current()?->parameters() ?? [];

return // use the entity name to get the current entry
// this makes sure the ID is current even for nested resources
Expand All @@ -48,6 +49,17 @@ public function getCurrentEntry()
return $this->getEntry($id);
}

public function getCurrentEntryWithLocale()
{
$entry = $this->getCurrentEntry();

if (! $entry) {
return false;
}

return $this->setLocaleOnModel($entry);
}

/**
* Find and retrieve an entry in the database or fail.
*
Expand All @@ -64,6 +76,13 @@ public function getEntry($id)
return $this->entry;
}

private function shouldUseFallbackLocale(): bool|string
{
$fallbackRequestValue = $this->getRequest()->get('_fallback_locale');

return $fallbackRequestValue === 'true' ? true : (in_array($fallbackRequestValue, array_keys(config('backpack.crud.locales'))) ? $fallbackRequestValue : false);
}

/**
* Find and retrieve an entry in the database or fail.
* When found, make sure we set the Locale on it.
Expand All @@ -77,14 +96,7 @@ public function getEntryWithLocale($id)
$this->entry = $this->getEntry($id);
}

if ($this->entry->translationEnabled()) {
$locale = request('_locale', \App::getLocale());
if (in_array($locale, array_keys($this->entry->getAvailableLocales()))) {
$this->entry->setLocale($locale);
}
}

return $this->entry;
return $this->setLocaleOnModel($this->entry);
}

/**
Expand Down Expand Up @@ -127,7 +139,7 @@ public function autoEagerLoadRelationshipColumns()
try {
$model = $model->$part()->getRelated();
} catch (Exception $e) {
$relation = join('.', array_slice($parts, 0, $i));
$relation = implode('.', array_slice($parts, 0, $i));
}
}
}
Expand Down
41 changes: 13 additions & 28 deletions src/app/Library/CrudPanel/Traits/Update.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private function updateModelAndRelations(Model $item, array $directInputs, array
public function getUpdateFields($id = false)
{
$fields = $this->fields();
$entry = ($id != false) ? $this->getEntry($id) : $this->getCurrentEntry();
$entry = ($id != false) ? $this->getEntryWithLocale($id) : $this->getCurrentEntryWithLocale();

foreach ($fields as &$field) {
$field['value'] = $field['value'] ?? $this->getModelAttributeValue($entry, $field);
Expand Down Expand Up @@ -126,7 +126,7 @@ private function getModelAttributeValueFromRelationship($model, $field)
$result = collect();

foreach ($relationModels as $model) {
$model = $this->setupRelatedModelLocale($model);
$model = $this->setLocaleOnModel($model);
// when subfields are NOT set we don't need to get any more values
// we just return the plain models as we only need the ids
if (! isset($field['subfields'])) {
Expand Down Expand Up @@ -167,7 +167,7 @@ private function getModelAttributeValueFromRelationship($model, $field)
return;
}

$model = $this->setupRelatedModelLocale($model);
$model = $this->setLocaleOnModel($model);
$model = $this->getModelWithFakes($model);

// if `entity` contains a dot here it means developer added a main HasOne/MorphOne relation with dot notation
Expand Down Expand Up @@ -195,24 +195,6 @@ private function getModelAttributeValueFromRelationship($model, $field)
}
}

/**
* Set the locale on the related models.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
private function setupRelatedModelLocale($model)
{
if (method_exists($model, 'translationEnabled') && $model->translationEnabled()) {
$locale = request('_locale', \App::getLocale());
if (in_array($locale, array_keys($model->getAvailableLocales()))) {
$model->setLocale($locale);
}
}

return $model;
}

/**
* This function checks if the provided model uses the CrudTrait.
* If IT DOES it adds the fakes to the model attributes.
Expand Down Expand Up @@ -274,12 +256,10 @@ private function getSubfieldsValues($subfields, $relatedModel)
if (! Str::contains($name, '.')) {
// when subfields are present, $relatedModel->{$name} returns a model instance
// otherwise returns the model attribute.
if ($relatedModel->{$name}) {
if (isset($subfield['subfields'])) {
$result[$name] = [$relatedModel->{$name}->only(array_column($subfield['subfields'], 'name'))];
} else {
$result[$name] = $relatedModel->{$name};
}
if ($relatedModel->{$name} && isset($subfield['subfields'])) {
$result[$name] = [$relatedModel->{$name}->only(array_column($subfield['subfields'], 'name'))];
} else {
$result[$name] = $relatedModel->{$name};
}
} else {
// if the subfield name contains a dot, we are going to iterate through
Expand All @@ -292,7 +272,12 @@ private function getSubfieldsValues($subfields, $relatedModel)
$iterator = $iterator->$part;
}

Arr::set($result, $name, is_a($iterator, 'Illuminate\Database\Eloquent\Model', true) ? $this->getModelWithFakes($iterator)->getAttributes() : $iterator);
if (is_a($iterator, 'Illuminate\Database\Eloquent\Model', true)) {
$iterator = $this->setLocaleOnModel($iterator);
$iterator = $this->getModelWithFakes($iterator)->getAttributes();
}

Arr::set($result, $name, $iterator);
}
}
}
Expand Down
14 changes: 9 additions & 5 deletions src/app/Models/Traits/SpatieTranslatable/HasTranslations.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Backpack\CRUD\app\Models\Traits\SpatieTranslatable;

use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Request;
use Spatie\Translatable\HasTranslations as OriginalHasTranslations;

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

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

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

// if it's a fake field, json_encode it
if (is_array($translation)) {
Expand Down Expand Up @@ -71,7 +75,7 @@ public function getTranslation(string $key, string $locale, bool $useFallbackLoc
*/
public static function create(array $attributes = [])
{
$locale = $attributes['locale'] ?? \App::getLocale();
$locale = $attributes['locale'] ?? App::getLocale();
$attributes = Arr::except($attributes, ['locale']);
$non_translatable = [];

Expand Down Expand Up @@ -103,7 +107,7 @@ public function update(array $attributes = [], array $options = [])
return false;
}

$locale = $attributes['_locale'] ?? \App::getLocale();
$locale = $attributes['_locale'] ?? App::getLocale();
$attributes = Arr::except($attributes, ['_locale']);
$non_translatable = [];

Expand Down Expand Up @@ -168,7 +172,7 @@ public function getLocale()
return $this->locale;
}

return \Request::input('_locale', \App::getLocale());
return Request::input('_locale', App::getLocale());
}

/**
Expand All @@ -187,7 +191,7 @@ public function __call($method, $parameters)
case 'findMany':
case 'findBySlug':
case 'findBySlugOrFail':
$translation_locale = \Request::input('_locale', \App::getLocale());
$translation_locale = Request::input('_locale', App::getLocale());

if ($translation_locale) {
$item = parent::__call($method, $parameters);
Expand Down
4 changes: 4 additions & 0 deletions src/config/backpack/operations/update.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
// Should we warn a user before leaving the page with unsaved changes?
'warnBeforeLeaving' => false,

// when viewing the update form of an entry in a language that's not translated should Backpack show a notice
// that allows the user to fill the form from another language?
'showTranslationNotice' => true,

// Before saving the entry, how would you like the request to be stripped?
// - false - use Backpack's default (ONLY save inputs that have fields)
// - invokable class - custom stripping (the return should be an array with input names)
Expand Down
4 changes: 4 additions & 0 deletions src/resources/lang/en/crud.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,8 @@
'quick_button_ajax_error_message' => 'There was an error processing your request.',
'quick_button_ajax_success_title' => 'Request Completed!',
'quick_button_ajax_success_message' => 'Your request was completed with success.',

// translations
'no_attributes_translated' => 'This entry is not translated in :locale.',
'no_attributes_translated_href_text' => 'Fill inputs from :locale',
];
2 changes: 1 addition & 1 deletion src/resources/views/crud/columns/select_multiple.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@
@else
{{ $column['default'] ?? '-' }}
@endif
</span>
</span>
35 changes: 11 additions & 24 deletions src/resources/views/crud/edit.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,17 @@
{!! csrf_field() !!}
{!! method_field('PUT') !!}

@if ($crud->model->translationEnabled())
<div class="mb-2 text-right">
{{-- Single button --}}
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{trans('backpack::crud.language')}}: {{ $crud->model->getAvailableLocales()[request()->input('_locale')?request()->input('_locale'):App::getLocale()] }} &nbsp; <span class="caret"></span>
</button>
<ul class="dropdown-menu">
@foreach ($crud->model->getAvailableLocales() as $key => $locale)
<a class="dropdown-item" href="{{ url($crud->route.'/'.$entry->getKey().'/edit') }}?_locale={{ $key }}">{{ $locale }}</a>
@endforeach
</ul>
</div>
</div>
@endif
{{-- load the view from the application if it exists, otherwise load the one in the package --}}
@if(view()->exists('vendor.backpack.crud.form_content'))
@include('vendor.backpack.crud.form_content', ['fields' => $crud->fields(), 'action' => 'edit'])
@else
@include('crud::form_content', ['fields' => $crud->fields(), 'action' => 'edit'])
@endif
{{-- This makes sure that all field assets are loaded. --}}
<div class="d-none" id="parentLoadedAssets">{{ json_encode(Basset::loaded()) }}</div>
@include('crud::inc.form_save_buttons')
@includeWhen($crud->model->translationEnabled(), 'crud::inc.edit_translation_notice')

{{-- load the view from the application if it exists, otherwise load the one in the package --}}
@if(view()->exists('vendor.backpack.crud.form_content'))
@include('vendor.backpack.crud.form_content', ['fields' => $crud->fields(), 'action' => 'edit'])
@else
@include('crud::form_content', ['fields' => $crud->fields(), 'action' => 'edit'])
@endif
{{-- This makes sure that all field assets are loaded. --}}
<div class="d-none" id="parentLoadedAssets">{{ json_encode(Basset::loaded()) }}</div>
@include('crud::inc.form_save_buttons')
</form>
</div>
</div>
Expand Down
Loading

0 comments on commit 761733f

Please sign in to comment.