Skip to content

Commit 761733f

Browse files
authored
Merge pull request #4267 from Laravel-Backpack/translatable-with-fallbacks
Translatable with fallbacks
2 parents 043cd5c + c378837 commit 761733f

File tree

9 files changed

+181
-78
lines changed

9 files changed

+181
-78
lines changed

src/app/Library/CrudPanel/CrudPanel.php

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,16 @@ public function getRequest()
105105
*
106106
* @param string $model_namespace Full model namespace. Ex: App\Models\Article
107107
*
108-
* @throws \Exception in case the model does not exist
108+
* @throws Exception in case the model does not exist
109109
*/
110110
public function setModel($model_namespace)
111111
{
112112
if (! class_exists($model_namespace)) {
113-
throw new \Exception('The model does not exist.', 500);
113+
throw new Exception('The model does not exist.', 500);
114114
}
115115

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

120120
$this->model = new $model_namespace();
@@ -198,7 +198,7 @@ public function setRoute($route)
198198
* @param string $route Route name.
199199
* @param array $parameters Parameters.
200200
*
201-
* @throws \Exception
201+
* @throws Exception
202202
*/
203203
public function setRouteName($route, $parameters = [])
204204
{
@@ -207,7 +207,7 @@ public function setRouteName($route, $parameters = [])
207207
$complete_route = $route.'.index';
208208

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

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

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

448449
if (! is_array($value)) {
449-
$decodedAttribute = json_decode($value, true);
450+
$decodedAttribute = json_decode($value, true) ?? ($value !== null ? [$value] : []);
450451
} else {
451452
$decodedAttribute = $value;
452453
}
@@ -462,11 +463,26 @@ public function parseTranslatableAttributes($model, $attribute, $value)
462463
return $value;
463464
}
464465

466+
public function setLocaleOnModel(Model $model)
467+
{
468+
$useFallbackLocale = $this->shouldUseFallbackLocale();
469+
470+
if (method_exists($model, 'translationEnabled') && $model->translationEnabled()) {
471+
$locale = $this->getRequest()->input('_locale', app()->getLocale());
472+
if (in_array($locale, array_keys($model->getAvailableLocales()))) {
473+
$model->setLocale(! is_bool($useFallbackLocale) ? $useFallbackLocale : $locale);
474+
$model->useFallbackLocale = (bool) $useFallbackLocale;
475+
}
476+
}
477+
478+
return $model;
479+
}
480+
465481
/**
466482
* Traverse the tree of relations for the given model, defined by the given relation string, and return the ending
467483
* associated model instance or instances.
468484
*
469-
* @param \Illuminate\Database\Eloquent\Model $model The CRUD model.
485+
* @param Model $model The CRUD model.
470486
* @param string $relationString Relation string. A dot notation can be used to chain multiple relations.
471487
* @return array An array of the associated model instances defined by the relation string.
472488
*/

src/app/Library/CrudPanel/Traits/Read.php

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Backpack\CRUD\app\Exceptions\BackpackProRequiredException;
66
use Exception;
7+
use Illuminate\Support\Facades\Route;
78

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

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

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

52+
public function getCurrentEntryWithLocale()
53+
{
54+
$entry = $this->getCurrentEntry();
55+
56+
if (! $entry) {
57+
return false;
58+
}
59+
60+
return $this->setLocaleOnModel($entry);
61+
}
62+
5163
/**
5264
* Find and retrieve an entry in the database or fail.
5365
*
@@ -64,6 +76,13 @@ public function getEntry($id)
6476
return $this->entry;
6577
}
6678

79+
private function shouldUseFallbackLocale(): bool|string
80+
{
81+
$fallbackRequestValue = $this->getRequest()->get('_fallback_locale');
82+
83+
return $fallbackRequestValue === 'true' ? true : (in_array($fallbackRequestValue, array_keys(config('backpack.crud.locales'))) ? $fallbackRequestValue : false);
84+
}
85+
6786
/**
6887
* Find and retrieve an entry in the database or fail.
6988
* When found, make sure we set the Locale on it.
@@ -77,14 +96,7 @@ public function getEntryWithLocale($id)
7796
$this->entry = $this->getEntry($id);
7897
}
7998

80-
if ($this->entry->translationEnabled()) {
81-
$locale = request('_locale', \App::getLocale());
82-
if (in_array($locale, array_keys($this->entry->getAvailableLocales()))) {
83-
$this->entry->setLocale($locale);
84-
}
85-
}
86-
87-
return $this->entry;
99+
return $this->setLocaleOnModel($this->entry);
88100
}
89101

90102
/**
@@ -127,7 +139,7 @@ public function autoEagerLoadRelationshipColumns()
127139
try {
128140
$model = $model->$part()->getRelated();
129141
} catch (Exception $e) {
130-
$relation = join('.', array_slice($parts, 0, $i));
142+
$relation = implode('.', array_slice($parts, 0, $i));
131143
}
132144
}
133145
}

src/app/Library/CrudPanel/Traits/Update.php

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ private function updateModelAndRelations(Model $item, array $directInputs, array
5151
public function getUpdateFields($id = false)
5252
{
5353
$fields = $this->fields();
54-
$entry = ($id != false) ? $this->getEntry($id) : $this->getCurrentEntry();
54+
$entry = ($id != false) ? $this->getEntryWithLocale($id) : $this->getCurrentEntryWithLocale();
5555

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

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

170-
$model = $this->setupRelatedModelLocale($model);
170+
$model = $this->setLocaleOnModel($model);
171171
$model = $this->getModelWithFakes($model);
172172

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

198-
/**
199-
* Set the locale on the related models.
200-
*
201-
* @param \Illuminate\Database\Eloquent\Model $model
202-
* @return \Illuminate\Database\Eloquent\Model
203-
*/
204-
private function setupRelatedModelLocale($model)
205-
{
206-
if (method_exists($model, 'translationEnabled') && $model->translationEnabled()) {
207-
$locale = request('_locale', \App::getLocale());
208-
if (in_array($locale, array_keys($model->getAvailableLocales()))) {
209-
$model->setLocale($locale);
210-
}
211-
}
212-
213-
return $model;
214-
}
215-
216198
/**
217199
* This function checks if the provided model uses the CrudTrait.
218200
* If IT DOES it adds the fakes to the model attributes.
@@ -274,12 +256,10 @@ private function getSubfieldsValues($subfields, $relatedModel)
274256
if (! Str::contains($name, '.')) {
275257
// when subfields are present, $relatedModel->{$name} returns a model instance
276258
// otherwise returns the model attribute.
277-
if ($relatedModel->{$name}) {
278-
if (isset($subfield['subfields'])) {
279-
$result[$name] = [$relatedModel->{$name}->only(array_column($subfield['subfields'], 'name'))];
280-
} else {
281-
$result[$name] = $relatedModel->{$name};
282-
}
259+
if ($relatedModel->{$name} && isset($subfield['subfields'])) {
260+
$result[$name] = [$relatedModel->{$name}->only(array_column($subfield['subfields'], 'name'))];
261+
} else {
262+
$result[$name] = $relatedModel->{$name};
283263
}
284264
} else {
285265
// if the subfield name contains a dot, we are going to iterate through
@@ -292,7 +272,12 @@ private function getSubfieldsValues($subfields, $relatedModel)
292272
$iterator = $iterator->$part;
293273
}
294274

295-
Arr::set($result, $name, is_a($iterator, 'Illuminate\Database\Eloquent\Model', true) ? $this->getModelWithFakes($iterator)->getAttributes() : $iterator);
275+
if (is_a($iterator, 'Illuminate\Database\Eloquent\Model', true)) {
276+
$iterator = $this->setLocaleOnModel($iterator);
277+
$iterator = $this->getModelWithFakes($iterator)->getAttributes();
278+
}
279+
280+
Arr::set($result, $name, $iterator);
296281
}
297282
}
298283
}

src/app/Models/Traits/SpatieTranslatable/HasTranslations.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Backpack\CRUD\app\Models\Traits\SpatieTranslatable;
44

55
use Illuminate\Support\Arr;
6+
use Illuminate\Support\Facades\App;
7+
use Illuminate\Support\Facades\Request;
68
use Spatie\Translatable\HasTranslations as OriginalHasTranslations;
79

810
trait HasTranslations
@@ -32,7 +34,9 @@ public function getAttributeValue($key)
3234
return parent::getAttributeValue($key);
3335
}
3436

35-
$translation = $this->getTranslation($key, $this->locale ?: config('app.locale'));
37+
$useFallbackLocale = property_exists($this, 'useFallbackLocale') ? $this->useFallbackLocale : true;
38+
39+
$translation = $this->getTranslation($key, $this->locale ?: config('app.locale'), $useFallbackLocale);
3640

3741
// if it's a fake field, json_encode it
3842
if (is_array($translation)) {
@@ -71,7 +75,7 @@ public function getTranslation(string $key, string $locale, bool $useFallbackLoc
7175
*/
7276
public static function create(array $attributes = [])
7377
{
74-
$locale = $attributes['locale'] ?? \App::getLocale();
78+
$locale = $attributes['locale'] ?? App::getLocale();
7579
$attributes = Arr::except($attributes, ['locale']);
7680
$non_translatable = [];
7781

@@ -103,7 +107,7 @@ public function update(array $attributes = [], array $options = [])
103107
return false;
104108
}
105109

106-
$locale = $attributes['_locale'] ?? \App::getLocale();
110+
$locale = $attributes['_locale'] ?? App::getLocale();
107111
$attributes = Arr::except($attributes, ['_locale']);
108112
$non_translatable = [];
109113

@@ -168,7 +172,7 @@ public function getLocale()
168172
return $this->locale;
169173
}
170174

171-
return \Request::input('_locale', \App::getLocale());
175+
return Request::input('_locale', App::getLocale());
172176
}
173177

174178
/**
@@ -187,7 +191,7 @@ public function __call($method, $parameters)
187191
case 'findMany':
188192
case 'findBySlug':
189193
case 'findBySlugOrFail':
190-
$translation_locale = \Request::input('_locale', \App::getLocale());
194+
$translation_locale = Request::input('_locale', App::getLocale());
191195

192196
if ($translation_locale) {
193197
$item = parent::__call($method, $parameters);

src/config/backpack/operations/update.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
// Should we warn a user before leaving the page with unsaved changes?
3636
'warnBeforeLeaving' => false,
3737

38+
// when viewing the update form of an entry in a language that's not translated should Backpack show a notice
39+
// that allows the user to fill the form from another language?
40+
'showTranslationNotice' => true,
41+
3842
// Before saving the entry, how would you like the request to be stripped?
3943
// - false - use Backpack's default (ONLY save inputs that have fields)
4044
// - invokable class - custom stripping (the return should be an array with input names)

src/resources/lang/en/crud.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,8 @@
196196
'quick_button_ajax_error_message' => 'There was an error processing your request.',
197197
'quick_button_ajax_success_title' => 'Request Completed!',
198198
'quick_button_ajax_success_message' => 'Your request was completed with success.',
199+
200+
// translations
201+
'no_attributes_translated' => 'This entry is not translated in :locale.',
202+
'no_attributes_translated_href_text' => 'Fill inputs from :locale',
199203
];

src/resources/views/crud/columns/select_multiple.blade.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@
4747
@else
4848
{{ $column['default'] ?? '-' }}
4949
@endif
50-
</span>
50+
</span>

src/resources/views/crud/edit.blade.php

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,17 @@
3939
{!! csrf_field() !!}
4040
{!! method_field('PUT') !!}
4141

42-
@if ($crud->model->translationEnabled())
43-
<div class="mb-2 text-right">
44-
{{-- Single button --}}
45-
<div class="btn-group">
46-
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
47-
{{trans('backpack::crud.language')}}: {{ $crud->model->getAvailableLocales()[request()->input('_locale')?request()->input('_locale'):App::getLocale()] }} &nbsp; <span class="caret"></span>
48-
</button>
49-
<ul class="dropdown-menu">
50-
@foreach ($crud->model->getAvailableLocales() as $key => $locale)
51-
<a class="dropdown-item" href="{{ url($crud->route.'/'.$entry->getKey().'/edit') }}?_locale={{ $key }}">{{ $locale }}</a>
52-
@endforeach
53-
</ul>
54-
</div>
55-
</div>
56-
@endif
57-
{{-- load the view from the application if it exists, otherwise load the one in the package --}}
58-
@if(view()->exists('vendor.backpack.crud.form_content'))
59-
@include('vendor.backpack.crud.form_content', ['fields' => $crud->fields(), 'action' => 'edit'])
60-
@else
61-
@include('crud::form_content', ['fields' => $crud->fields(), 'action' => 'edit'])
62-
@endif
63-
{{-- This makes sure that all field assets are loaded. --}}
64-
<div class="d-none" id="parentLoadedAssets">{{ json_encode(Basset::loaded()) }}</div>
65-
@include('crud::inc.form_save_buttons')
42+
@includeWhen($crud->model->translationEnabled(), 'crud::inc.edit_translation_notice')
43+
44+
{{-- load the view from the application if it exists, otherwise load the one in the package --}}
45+
@if(view()->exists('vendor.backpack.crud.form_content'))
46+
@include('vendor.backpack.crud.form_content', ['fields' => $crud->fields(), 'action' => 'edit'])
47+
@else
48+
@include('crud::form_content', ['fields' => $crud->fields(), 'action' => 'edit'])
49+
@endif
50+
{{-- This makes sure that all field assets are loaded. --}}
51+
<div class="d-none" id="parentLoadedAssets">{{ json_encode(Basset::loaded()) }}</div>
52+
@include('crud::inc.form_save_buttons')
6653
</form>
6754
</div>
6855
</div>

0 commit comments

Comments
 (0)