Skip to content

Commit

Permalink
Merge branch 'staging'
Browse files Browse the repository at this point in the history
  • Loading branch information
petrzpav committed Feb 19, 2025
2 parents 3640c2d + 1aae4b8 commit 1eda5e9
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 51 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [0.2.0] - 2025-02-19

_Stable release based on [0.2.0-rc.1]._

## [0.2.0-rc.1] - 2025-02-19

## [0.1.1] - 2024-10-28

### Fixed
Expand All @@ -22,6 +28,8 @@ _Stable release based on [0.1.0-rc.1]._

- New changelog file.

[0.2.0]: https://https://github.com/internetguru/laravel-translatable/compare/v0.1.1...v0.2.0
[0.2.0-rc.1]: https://github.com/internetguru/laravel-translatable/releases/tag/v0.1.1
[0.1.1]: https://https://github.com/internetguru/laravel-translatable/compare/v0.1.0...v0.1.1
[0.1.0]: https://https://github.com/internetguru/laravel-translatable/compare/v0.0.0...v0.1.0
[0.1.0-rc.1]: https://github.com/internetguru/laravel-translatable/releases/tag/v0.0.0
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.1
0.2.0
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"name": "internetguru/laravel-translatable",
"type": "library",
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"laravel/framework": "^11.0"
},
Expand Down
124 changes: 77 additions & 47 deletions src/Traits/Translatable.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,63 +7,47 @@

trait Translatable
{
public function translations()
const NULL_PLACEHOLDER = '__NULL__';

const TRANSLATABLE_CACHE_TTL = 60 * 60 * 24;

protected bool $isInitializing = true;

public function __construct(array $attributes = [])
{
return $this->morphMany(Translation::class, 'translatable');
parent::__construct($attributes);
$this->isInitializing = false;
}

/**
* Translate the given attribute to the given locale.
* Fallback to the given fallback locale.
* Fallback to any other first locale.
* Fallback to null.
*/
public function translate($attribute, $locale, $fallbackLocale)
public function translations()
{
$cacheKey = $this->getTraslationCacheKey($attribute, $locale, $fallbackLocale);

return Cache::remember($cacheKey, 60, function () use ($attribute, $locale, $fallbackLocale) {
$translations = $this->translations()->where('attribute', $attribute)->get();
if ($translations->isEmpty()) {
return null;
}
if ($translation = $translations->where('locale', $locale)->first()) {
return $translation->value;
}
if ($translation = $translations->where('locale', $fallbackLocale)->first()) {
return $translation->value;
}

return $translations->first()->value;
});
return $this->morphMany(Translation::class, 'translatable');
}

/**
* Get all translatable attributes.
*/
public function getTranslatableAttributes()
{
return $this->translatable ?? [];
}

/**
* Get the attribute from the model.
* Use translation if exists.
*/
public function getAttribute($key)
{
if (! in_array($key, $this->getTranslatableAttributes())) {
return parent::getAttribute($key);
}

return $this->translate($key, app()->getLocale(), app()->getFallbackLocale());
}

public function getAttributeTranslations($key, $useFallbackLocale = true)
{
foreach (config('languages') as $locale => $language) {
$fallbackLocale = $useFallbackLocale ? app()->getFallbackLocale() : $locale;
$translations[$locale] = $this->translate($key, $locale, $fallbackLocale);
}

return $translations;
}

/**
* Set the attribute to the model.
* Save translation if attribute is translatable.
*/
public function setAttribute($key, $value)
{
if (! in_array($key, $this->getTranslatableAttributes())) {
Expand All @@ -73,21 +57,67 @@ public function setAttribute($key, $value)
}

$locale = app()->getLocale();
$fallbackLocale = app()->getFallbackLocale();
$this->translations()->updateOrCreate(
['attribute' => $key, 'locale' => $locale],
['value' => $value]
);
Cache::forget($this->getTraslationCacheKey($key, $locale, $fallbackLocale));
// do not set translations into database during initialization phase
if (! $this->isInitializing) {
$this->setTranslation($key, $locale, $value);
}

return $this;
}

/**
* Get the cache key for the translation.
*/
private function getTraslationCacheKey($attribute, $locale, $fallbackLocale)
public function translate($attribute, $locale, $fallbackLocale)
{
$cacheKey = $this->getTranslationCacheKey($attribute, $locale, $fallbackLocale);
$cachedValue = Cache::get($cacheKey);

// use NULL_PLACEHOLDER to cache null values
if ($cachedValue === self::NULL_PLACEHOLDER) {
return null;
}
if ($cachedValue !== null) {
return $cachedValue;
}

$value = $this->computeTranslation($attribute, $locale, $fallbackLocale);
Cache::put($cacheKey, $value === null ? self::NULL_PLACEHOLDER : $value, self::TRANSLATABLE_CACHE_TTL);

return $value;
}

protected function computeTranslation($attribute, $locale, $fallbackLocale)
{
$translations = $this->translations()->where('attribute', $attribute)->get();
if ($translations->isEmpty()) {
return null;
}
if ($translation = $translations->where('locale', $locale)->first()) {
return $translation->value;
}
if ($translation = $translations->where('locale', $fallbackLocale)->first()) {
return $translation->value;
}

return null;
}

public function setTranslation($attribute, $locale, $value)
{
if ($value) {
$this->translations()->updateOrCreate(
['attribute' => $attribute, 'locale' => $locale],
['value' => $value]
);
} else {
$this->translations()->where('attribute', $attribute)->where('locale', $locale)->delete();
}

$fallbackLocale = app()->getFallbackLocale();
Cache::forget($this->getTranslationCacheKey($attribute, $locale, $fallbackLocale));
Cache::forget($this->getTranslationCacheKey($attribute, $locale, $locale));
}

protected function getTranslationCacheKey($attribute, $locale, $fallbackLocale)
{
return "translation_{$this->id}_{$attribute}_{$locale}_{$fallbackLocale}";
return 'translation_' . get_class($this) . "_{$this->id}_{$attribute}_{$locale}_{$fallbackLocale}";
}
}
9 changes: 6 additions & 3 deletions tests/Traits/TranslatableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function test_translate_not_found()
$this->assertNull($room->translate('description', 'fr', 'en'));
}

public function test_translate_function_fallback_to_first()
public function test_translate_function_not_fallback_to_first()
{
$room = Room::factory()->create();
app()->setLocale('fr');
Expand All @@ -38,7 +38,7 @@ public function test_translate_function_fallback_to_first()

app()->setLocale('en');
$this->assertEquals('Description Traduit', $room->translate('description', 'fr', 'en'));
$this->assertEquals('Description Traduit', $room->translate('description', 'en', 'en'));
$this->assertEquals(null, $room->translate('description', 'en', 'en'));
}

public function test_implicit_get_attribute_with_translation()
Expand Down Expand Up @@ -79,7 +79,10 @@ public function test_set_attribute_with_translation()

Cache::shouldReceive('forget')
->once()
->with("translation_{$room->id}_description_fr_en");
->with("translation_App\\Models\\Room_{$room->id}_description_fr_fr");
Cache::shouldReceive('forget')
->once()
->with("translation_App\\Models\\Room_{$room->id}_description_fr_en");

$room->setAttribute('description', 'Nouvelle Description Traduit');
$this->assertDatabaseHas('translations', [
Expand Down

0 comments on commit 1eda5e9

Please sign in to comment.